You are viewing a plain text version of this content. The canonical link for it is here.
Posted to pr@cassandra.apache.org by GitBox <gi...@apache.org> on 2020/07/30 12:00:27 UTC

[GitHub] [cassandra] bereng opened a new pull request #704: Cassandra 15991

bereng opened a new pull request #704:
URL: https://github.com/apache/cassandra/pull/704


   


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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484196163



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))

Review comment:
       Atm {{ToolRunner}} is {{AutoClosable}} so it's good to close it in case in the future `close()` does sthg meaningful imo. Let's see how `ToolRunner` looks when we finish our discussions about it first, then we can revert/change this as it's a pain to change every time we go back/forth on it.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on pull request #704:
URL: https://github.com/apache/cassandra/pull/704#issuecomment-684009699


   Started but this patch is very large so will take a while to review.
   
   General comments are:
   * avoid using assertTrue/assertFalse as they produce bad errors (unless you control the errors)
   * prefer Assertions.assertThat as it produces useful error messages
   * when validating that the usage hasn't changed, should use the string usage rather than a hash of it, as the hash is harder to know what failed and how to resolve.
   * `WARNING: An illegal reflective access operation has occurred` is a bug, so we should fix this rather than add exclusions to the test.
   * when working with List you already have a forEach method, so no need to create a stream


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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486053062



##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -459,14 +480,24 @@ public ToolRunner invokeTool(String... args)
 
         public ToolRunner invokeTool(List<String> args)
         {
-            return invokeTool(args, true);
+            return invokeTool(args, true, true);
         }
 
-        public ToolRunner invokeTool(List<String> args, boolean runOutOfProcess)
+        public ToolRunner invokeToolNoWait(List<String> args)
+        {
+            return invokeTool(args, true, false);
+        }
+
+        public ToolRunner invokeTool(List<String> args, boolean runOutOfProcess, boolean wait)
         {
             ToolRunner runner = new ToolRunner(args, runOutOfProcess);
-            runner.start().waitFor();
+            if (wait)
+                runner.start().waitFor();

Review comment:
       Not seeing it unfortunately. Feel free to push a commit?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486060433



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
##########
@@ -52,5 +67,101 @@ public void testStandaloneSplitter_NoArgs()
         assertServerNotLoaded();
     }
 
-    // Note: StandaloneSplitter modifies sstables
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "-h"))
+        {
+            String help = "usage: sstablessplit [options] <filename> [<filename>]*\n" + 
+                          "--\n" + 
+                          "Split the provided sstables files in sstables of maximum provided file\n" + 
+                          "size (see option --size).\n" + 
+                          "--\n" + 
+                          "Options are:\n" + 
+                          "    --debug         display stack traces\n" + 
+                          " -h,--help          display this help message\n" + 
+                          "    --no-snapshot   don't snapshot the sstables before splitting\n" + 
+                          " -s,--size <size>   maximum size in MB for the output sstables (default:\n" + 
+                          "                    50)\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "--debugwrong", "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testWrongFilename()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+            assertEquals(1,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "--no-snapshot").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), arg, "mockFile"))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                assertEquals(1,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testSizeArg()
+    {
+        Arrays.asList(Pair.of("-s", ""), Pair.of("-s", "w"), Pair.of("--size", ""), Pair.of("--size", "w"))
+              .forEach(arg -> {
+                  try
+                  {
+                      runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
+                                               arg.getLeft(),
+                                               arg.getRight(),
+                                               "mockFile");
+                      fail("Shouldn't be able to parse wrong input as number");
+                  }
+                  catch(RuntimeException e)

Review comment:
       Given it's a nit, I can't 'bulk replace it' bc it needs manual edit on each file and that it's consistent with the rest of unit tests I'll leave as it is. But it's nice to see uses of the `assertThat` family.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r481003003



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+        Arrays.asList(Pair.of("-f", "-r"), Pair.of("--follow", "--roll_cycle")).stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                    path.toAbsolutePath().toString(),
+                                                                    arg.getLeft(),
+                                                                    arg.getRight(),
+                                                                    "TEST_SECONDLY"));
+            // Tool is running in the background 'following' so we have to kill it
+            assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       Aaaaand this is me being silly where `ToolRunner` was made `AutoClosable` and I didn't close it even 1 time. :facepalm: Apologies. Fixed across the PR.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484198394



##########
File path: test/unit/org/apache/cassandra/tools/ClearSnapshotTest.java
##########
@@ -54,32 +56,43 @@ public static void teardown() throws IOException
     @Test
     public void testClearSnapshot_NoArgs() throws IOException
     {
-        ToolRunner tool = runner.invokeNodetool("clearsnapshot");
-        assertEquals(2, tool.getExitCode());
-        assertTrue("Tool stderr: " +  tool.getStderr(), tool.getStderr().contains("Specify snapshot name or --all"));
+        try (ToolRunner tool = runner.invokeNodetool("clearsnapshot"))

Review comment:
       Referring back to another previous comment. Let's leave this as it is now if you don't mind until we have a final `ToolRunner` version to stick with?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486046946



##########
File path: test/unit/org/apache/cassandra/tools/cqlsh/CqlshTest.java
##########
@@ -0,0 +1,53 @@
+/*
+ * 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.cqlsh;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+import org.hamcrest.CoreMatchers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class CqlshTest extends CQLTester
+{
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
+    @BeforeClass
+    public static void setUp()
+    {
+        requireNetwork();
+    }
+
+    @Test
+    public void testKeyspaceRequired() throws IOException

Review comment:
       Thx my IDE didn't warm these ones... mmmm




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486070233



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+            try (ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                         path.toAbsolutePath().toString(),
+                                                                         arg.get(0),
+                                                                         arg.get(1),
+                                                                         "TEST_SECONDLY")))
+            {
+                // Tool is running in the background 'following' so we have to kill it
+                assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       I hope once we settle on `ToolRunner` your review should be minimal as you'll only have to scroll across the test files as the only change will be the API changes.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r494632308



##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();

Review comment:
       since ObservableTool is closable can you make this
   
   ```
   try (ObservableTool  t = invokeAsync(args))
   {
       return t.waitComplete();
   }
   ```

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();

Review comment:
       since ObservableTool is closable can you make this
   
   ```
   try (ObservableTool  t = invokeAsync(env, stdin, args))
   {
       return t.waitComplete();
   }
   ```

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);

Review comment:
       `newOut` and `newErr` are buffered so should call `.flush()` before calling `.toString()` on the backing byte array stream; in the common println case this wouldn't be an issue, but if a tool does print and fails in the middle then this might not show up in the output.

##########
File path: test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java
##########
@@ -43,9 +57,112 @@ public void testSSTableOfflineRelevel_NoArgs()
     }
 
     @Test
-    public void testSSTableOfflineRelevel_WithArgs()
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolResult tool = ToolRunner.invokeClass(SSTableMetadataViewer.class, "-h");
+        assertEquals("a30d3c489bcf56ddb2140184c5c62422", DigestUtils.md5Hex(tool.getCleanedStderr()));

Review comment:
       can you add the help string here rather than md5?

##########
File path: test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
##########
@@ -75,20 +73,20 @@ public void testBulkLoader_WithArgs() throws Exception
     @Test
     public void testBulkLoader_WithArgs1() throws Exception
     {
-        try
-        {
-            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)
-        {
-            if (!(e.getCause() instanceof BulkLoadException))
-                throw e;
-            if (!(e.getCause().getCause() instanceof NoHostAvailableException))
-                throw e;
-        }
-        assertNoUnexpectedThreadsStarted(null, new String[]{"globalEventExecutor-1-1", "globalEventExecutor-1-2"});
+        ToolResult tool = ToolRunner.invokeClass(BulkLoader.class,
+                                                 "-d",
+                                                 "127.9.9.1",
+                                                 "--port",
+                                                 "9042",
+                                                 OfflineToolUtils.sstableDirName("legacy_sstables", "legacy_ma_simple"));
+
+        assertEquals(-1, tool.getExitCode());
+        if (!(tool.getException().getCause() instanceof BulkLoadException))

Review comment:
       nit: I think you can do the same with
   
   ```
   Assertions.assertThat(tool.getExitCode())
                     .hasCauseInstanceOf(BulkLoadException.class)
                     .hasRootCauseInstanceOf(NoHostAvailableException.class); // no host normally doesn't have a cause so this should match.
   ```

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);
+        }
+        catch (Exception e)
+        {
+            return new ToolResult(allArgs, -1, "", Throwables.getStackTraceAsString(e), e);

Review comment:
       shouldn't we use `out.toString(), err.toString()` as well?  if we NPE but log a lot, we would miss the log even though we have the exception in `ToolResult`

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);
+        }
+        catch (Exception e)
+        {
+            return new ToolResult(allArgs, -1, "", Throwables.getStackTraceAsString(e), e);
+        }
+        finally
+        {
+            System.setOut(originalSysOut);
+            System.setErr(originalSysErr);
+            System.setIn(originalSysIn);
         }
     }
 
-    private static final class StreamGobbler<T extends OutputStream> implements Runnable
+    public static Builder builder(List<String> args)
     {
-        private static final int BUFFER_SIZE = 8_192;
+        return new Builder(args);
+    }
 
-        private final InputStream input;
-        private final T out;
+    public static final class ToolResult
+    {
+        private final List<String> allArgs;
+        private final int exitCode;
+        private final String stdout;
+        private final String stderr;
+        private final Exception e;
 
-        private StreamGobbler(InputStream input, T out)
+        private ToolResult(List<String> allArgs, int exitCode, String stdout, String stderr, Exception e)
         {
-            this.input = input;
-            this.out = out;
+            this.allArgs = allArgs;
+            this.exitCode = exitCode;
+            this.stdout = stdout;
+            this.stderr = stderr;
+            this.e = e;
         }
 
-        public void run()
+        public int getExitCode()
         {
-            byte[] buffer = new byte[BUFFER_SIZE];
-            while (true)
-            {
-                try
-                {
-                    int read = input.read(buffer);
-                    if (read == -1)
-                    {
-                        return;
-                    }
-                    out.write(buffer, 0, read);
-                }
-                catch (IOException e)
-                {
-                    logger.error("Unexpected IO Error while reading stream", e);
-                    return;
-                }
-            }
+            return exitCode;
+        }
+
+        public String getStdout()
+        {
+            return stdout;
+        }
+
+        public String getStderr()
+        {
+            return stderr;
+        }
+        
+        public Exception getException()
+        {
+            return e;
         }
+        
+        /**
+         * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @return The ToolRunner instance
+         */
+        public void assertCleanStdErr()
+        {
+            assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(),
+                       getCleanedStderr().isEmpty());
+        }
+
+        public void assertOnExitCode()
+        {
+            assertExitCode(getExitCode());
+        }
+
+        private void assertExitCode(int code)
+        {
+            if (code != 0)
+                fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
+                                   argsToLogString(),
+                                   code,
+                                   getStderr(),
+                                   getStdout()));
+        }
+
+        public String argsToLogString()
+        {
+            return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output through the provided cleaners
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @param regExpCleaners
+         *            List of regExps to remove from stdErr
+         * @return The stdErr with all excludes removed
+         */
+        public String getCleanedStderr(List<String> regExpCleaners)
+        {
+            String sanitizedStderr = getStderr();
+            for (String regExp : regExpCleaners)
+                sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
+            return sanitizedStderr;
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output. Uses default list of excludes
+         * 
+         * {@link #getCleanedStderr(List)}
+         */
+        public String getCleanedStderr()
+        {
+            return getCleanedStderr(DEFAULT_CLEANERS);
+        }
+        
+        public void assertOnCleanExit()
+        {
+            assertOnExitCode();
+            assertCleanStdErr();
+        }
+    }
+
+    public interface ObservableTool extends AutoCloseable
+    {
+        String getPartialStdout();
+
+        String getPartialStderr();
+
+        boolean isDone();
+
+        ToolResult waitComplete();
+
+        @Override
+        void close();
     }
 
-    public static class Runners
+    private static final class FailedObservableTool implements ObservableTool
     {
-        public static ToolRunner invokeNodetool(String... args)
+        private final List<String> args;
+        private final IOException error;
+
+        private FailedObservableTool(IOException error, List<String> args)
+        {
+            this.args = args;
+            this.error = error;
+        }
+
+        @Override
+        public String getPartialStdout()
+        {
+            return "";
+        }
+
+        @Override
+        public String getPartialStderr()
+        {
+            return error.getMessage();
+        }
+
+        @Override
+        public boolean isDone()
         {
-            return invokeNodetool(Arrays.asList(args));
+            return true;
         }
 
-        public static ToolRunner invokeNodetool(List<String> args)
+        @Override
+        public ToolResult waitComplete()
         {
-            return invokeTool(buildNodetoolArgs(args), true);
+            return new ToolResult(args, -1, getPartialStdout(), getPartialStderr(), error);
         }
 
-        private static List<String> buildNodetoolArgs(List<String> args)
+        @Override
+        public void close()
         {
-            return CQLTester.buildNodetoolArgs(args);
+
         }
+    }
 
-        public static ToolRunner invokeClassAsTool(String... args)
+    private static final class ForkedObservableTool implements ObservableTool
+    {
+        private final CompletableFuture<Void> onComplete = new CompletableFuture<>();
+        private final ByteArrayOutputStream err = new ByteArrayOutputStream();

Review comment:
       would need to load in IntelliJ but I believe this would be an IntelliJ warning, so I think we should add `@SupressWarning("resources")`

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable

Review comment:
       is there a reason to make this generic?  I don't see us doing anything with it; I am ok if you want to keep it there.

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);
+        }
+        catch (Exception e)
+        {
+            return new ToolResult(allArgs, -1, "", Throwables.getStackTraceAsString(e), e);
+        }
+        finally
+        {
+            System.setOut(originalSysOut);
+            System.setErr(originalSysErr);
+            System.setIn(originalSysIn);
         }
     }
 
-    private static final class StreamGobbler<T extends OutputStream> implements Runnable
+    public static Builder builder(List<String> args)
     {
-        private static final int BUFFER_SIZE = 8_192;
+        return new Builder(args);
+    }
 
-        private final InputStream input;
-        private final T out;
+    public static final class ToolResult
+    {
+        private final List<String> allArgs;
+        private final int exitCode;
+        private final String stdout;
+        private final String stderr;
+        private final Exception e;
 
-        private StreamGobbler(InputStream input, T out)
+        private ToolResult(List<String> allArgs, int exitCode, String stdout, String stderr, Exception e)
         {
-            this.input = input;
-            this.out = out;
+            this.allArgs = allArgs;
+            this.exitCode = exitCode;
+            this.stdout = stdout;
+            this.stderr = stderr;
+            this.e = e;
         }
 
-        public void run()
+        public int getExitCode()
         {
-            byte[] buffer = new byte[BUFFER_SIZE];
-            while (true)
-            {
-                try
-                {
-                    int read = input.read(buffer);
-                    if (read == -1)
-                    {
-                        return;
-                    }
-                    out.write(buffer, 0, read);
-                }
-                catch (IOException e)
-                {
-                    logger.error("Unexpected IO Error while reading stream", e);
-                    return;
-                }
-            }
+            return exitCode;
+        }
+
+        public String getStdout()
+        {
+            return stdout;
+        }
+
+        public String getStderr()
+        {
+            return stderr;
+        }
+        
+        public Exception getException()
+        {
+            return e;
         }
+        
+        /**
+         * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @return The ToolRunner instance
+         */
+        public void assertCleanStdErr()
+        {
+            assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(),
+                       getCleanedStderr().isEmpty());
+        }
+
+        public void assertOnExitCode()
+        {
+            assertExitCode(getExitCode());
+        }
+
+        private void assertExitCode(int code)
+        {
+            if (code != 0)
+                fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
+                                   argsToLogString(),
+                                   code,
+                                   getStderr(),
+                                   getStdout()));
+        }
+
+        public String argsToLogString()
+        {
+            return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output through the provided cleaners
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @param regExpCleaners
+         *            List of regExps to remove from stdErr
+         * @return The stdErr with all excludes removed
+         */
+        public String getCleanedStderr(List<String> regExpCleaners)
+        {
+            String sanitizedStderr = getStderr();
+            for (String regExp : regExpCleaners)
+                sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
+            return sanitizedStderr;
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output. Uses default list of excludes
+         * 
+         * {@link #getCleanedStderr(List)}
+         */
+        public String getCleanedStderr()
+        {
+            return getCleanedStderr(DEFAULT_CLEANERS);
+        }
+        
+        public void assertOnCleanExit()
+        {
+            assertOnExitCode();
+            assertCleanStdErr();
+        }
+    }
+
+    public interface ObservableTool extends AutoCloseable
+    {
+        String getPartialStdout();
+
+        String getPartialStderr();
+
+        boolean isDone();
+
+        ToolResult waitComplete();
+
+        @Override
+        void close();
     }
 
-    public static class Runners
+    private static final class FailedObservableTool implements ObservableTool
     {
-        public static ToolRunner invokeNodetool(String... args)
+        private final List<String> args;
+        private final IOException error;
+
+        private FailedObservableTool(IOException error, List<String> args)
+        {
+            this.args = args;
+            this.error = error;
+        }
+
+        @Override
+        public String getPartialStdout()
+        {
+            return "";
+        }
+
+        @Override
+        public String getPartialStderr()
+        {
+            return error.getMessage();
+        }
+
+        @Override
+        public boolean isDone()
         {
-            return invokeNodetool(Arrays.asList(args));
+            return true;
         }
 
-        public static ToolRunner invokeNodetool(List<String> args)
+        @Override
+        public ToolResult waitComplete()
         {
-            return invokeTool(buildNodetoolArgs(args), true);
+            return new ToolResult(args, -1, getPartialStdout(), getPartialStderr(), error);
         }
 
-        private static List<String> buildNodetoolArgs(List<String> args)
+        @Override
+        public void close()
         {
-            return CQLTester.buildNodetoolArgs(args);
+
         }
+    }
 
-        public static ToolRunner invokeClassAsTool(String... args)
+    private static final class ForkedObservableTool implements ObservableTool
+    {
+        private final CompletableFuture<Void> onComplete = new CompletableFuture<>();

Review comment:
       I believe we talked about removing this in favor of just closing stdin directly in the other ticket; I am fine this way or directly.

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable

Review comment:
       did I do this in my POC?  If so I think I did it because I had some callback logic I eventually removed as it turned out to be less useful.

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +74,87 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolResult tool = ToolRunner.invoke(toolPath);
+        assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+        assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolResult tool = ToolRunner.invoke(toolPath, "-h");
+        String help = "usage: auditlogviewer <path1> [<path2>...<pathN>] [options]\n" + 
+                       "--\n" + 
+                       "View the audit log contents in human readable format\n" + 
+                       "--\n" + 
+                       "Options are:\n" + 
+                       " -f,--follow             Upon reacahing the end of the log continue\n" + 
+                       "                         indefinitely waiting for more records\n" + 
+                       " -h,--help               display this help message\n" + 
+                       " -i,--ignore             Silently ignore unsupported records\n" + 
+                       " -r,--roll_cycle <arg>   How often to roll the log file was rolled. May be\n" + 
+                       "                         necessary for Chronicle to correctly parse file names. (MINUTELY, HOURLY,\n" + 
+                       "                         DAILY). Default HOURLY.\n";
+        Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            ToolResult tool = ToolRunner.invoke(toolPath, arg);
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            ToolResult tool = ToolRunner.invoke(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            // @IgnoreAssert see CASSANDRA-16021
+//                assertTrue(tool.getCleanedStderr(),
+//                           tool.getCleanedStderr().isEmpty() // j8 is fine
+//                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+            tool.assertOnExitCode();
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+                ObservableTool tool = ToolRunner.invokeAsync(toolPath,

Review comment:
       since this is closable can you wrap in a try-with-resource block?

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +74,87 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolResult tool = ToolRunner.invoke(toolPath);
+        assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+        assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolResult tool = ToolRunner.invoke(toolPath, "-h");
+        String help = "usage: auditlogviewer <path1> [<path2>...<pathN>] [options]\n" + 
+                       "--\n" + 
+                       "View the audit log contents in human readable format\n" + 
+                       "--\n" + 
+                       "Options are:\n" + 
+                       " -f,--follow             Upon reacahing the end of the log continue\n" + 
+                       "                         indefinitely waiting for more records\n" + 
+                       " -h,--help               display this help message\n" + 
+                       " -i,--ignore             Silently ignore unsupported records\n" + 
+                       " -r,--roll_cycle <arg>   How often to roll the log file was rolled. May be\n" + 
+                       "                         necessary for Chronicle to correctly parse file names. (MINUTELY, HOURLY,\n" + 
+                       "                         DAILY). Default HOURLY.\n";
+        Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            ToolResult tool = ToolRunner.invoke(toolPath, arg);
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            ToolResult tool = ToolRunner.invoke(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            // @IgnoreAssert see CASSANDRA-16021
+//                assertTrue(tool.getCleanedStderr(),
+//                           tool.getCleanedStderr().isEmpty() // j8 is fine
+//                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+            tool.assertOnExitCode();
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+                ObservableTool tool = ToolRunner.invokeAsync(toolPath,

Review comment:
       the close on line 154 might not happen if the assert true throws.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r485065993



##########
File path: test/unit/org/apache/cassandra/cql3/CQLTester.java
##########
@@ -451,19 +451,43 @@ public void run()
             }
         });
     }
-    
+
     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.add(jmxHost == null ? "127.0.0.1" : jmxHost);

Review comment:
       makes sense, thanks.  In this case it may make more sense to also ignore `-h` but I will leave this comment as a "nit", so free to leave as is or change.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486046545



##########
File path: test/unit/org/apache/cassandra/tools/cqlsh/CqlshTest.java
##########
@@ -0,0 +1,53 @@
+/*
+ * 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.cqlsh;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+import org.hamcrest.CoreMatchers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class CqlshTest extends CQLTester
+{
+    private ToolRunner.Runners runner = new ToolRunner.Runners();

Review comment:
       will do across all PR.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r485917724



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+            try (ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                         path.toAbsolutePath().toString(),
+                                                                         arg.get(0),
+                                                                         arg.get(1),
+                                                                         "TEST_SECONDLY")))
+            {
+                // Tool is running in the background 'following' so we have to kill it
+                assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       > Would you be ok continuing the review assuming CASSANDRA-16082 + your POC will go in
   
   I can keep reviewing, but will need to re-review once you make the changes...
   
   >  I don't have a problem adopting your patch
   
   My patch was meant to be a POC and compile in isolation so I didn't need to worry about breaking anything.  Given this I am open to changes.  
   
   One thing that comes to mind is `ToolResult` was just to avoid refactoring all the code, it may make the patch smaller if ToolRunner was functionally the same as `ToolResult`?  I am not a stickler for names as much as I am for the ideas behind them.
   
   > I still prefer the try{...} approach but that is down to personal preference imo
   
   A common issue I see is that one person forgets try {...} (not with ToolRunner, but other logic) and someone else comes along to fix this.  If we can make the API easier to use than we need less people fixing the code after it is committed.
   
   Recent example: CASSANDRA-16119
   




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486708135



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
##########
@@ -52,5 +67,101 @@ public void testStandaloneSplitter_NoArgs()
         assertServerNotLoaded();
     }
 
-    // Note: StandaloneSplitter modifies sstables
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "-h"))
+        {
+            String help = "usage: sstablessplit [options] <filename> [<filename>]*\n" + 
+                          "--\n" + 
+                          "Split the provided sstables files in sstables of maximum provided file\n" + 
+                          "size (see option --size).\n" + 
+                          "--\n" + 
+                          "Options are:\n" + 
+                          "    --debug         display stack traces\n" + 
+                          " -h,--help          display this help message\n" + 
+                          "    --no-snapshot   don't snapshot the sstables before splitting\n" + 
+                          " -s,--size <size>   maximum size in MB for the output sstables (default:\n" + 
+                          "                    50)\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "--debugwrong", "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testWrongFilename()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+            assertEquals(1,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "--no-snapshot").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), arg, "mockFile"))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                assertEquals(1,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testSizeArg()
+    {
+        Arrays.asList(Pair.of("-s", ""), Pair.of("-s", "w"), Pair.of("--size", ""), Pair.of("--size", "w"))
+              .forEach(arg -> {
+                  try
+                  {
+                      runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
+                                               arg.getLeft(),
+                                               arg.getRight(),
+                                               "mockFile");
+                      fail("Shouldn't be able to parse wrong input as number");
+                  }
+                  catch(RuntimeException e)

Review comment:
       thats fine, mostly showing as an FYI




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486066462



##########
File path: test/unit/org/apache/cassandra/tools/SSTableExportTest.java
##########
@@ -43,12 +62,146 @@ public void testSSTableExport_NoArgs()
     }
 
     @Test
-    public void testSSTableExport_WithArgs() throws Exception
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableExport.class.getName()))
+        {
+            String help = "usage: sstabledump <sstable file path> <options>\n" + 
+                           "                   \n" + 
+                           "Dump contents of given SSTable to standard output in JSON format.\n" + 
+                           " -d         CQL row per line internal representation\n" + 
+                           " -e         enumerate partition keys only\n" + 
+                           " -k <arg>   Partition key\n" + 
+                           " -l         Output json lines, by partition\n" + 
+                           " -t         Print raw timestamps instead of iso8601 date strings\n" + 
+                           " -x <arg>   Excluded partition key\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableExport.class.getName(), "--debugwrong", findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport",findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertTrue(tool.getStdout(), parsed.get(0).get("partition") != null);
+            assertTrue(tool.getStdout(), parsed.get(0).get("rows") != null);
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testCQLRowArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-d"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.startsWith("[0]"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testPKOnlyArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-e"))
+        {
+            assertEquals(tool.getStdout(), "[ [ \"0\" ], [ \"1\" ], [ \"2\" ], [ \"3\" ], [ \"4\" ]\n]", tool.getStdout());
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testPKArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-k", "0"))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertEquals(tool.getStdout(), 1, parsed.size());
+            assertEquals(tool.getStdout(), "0", ((List) ((Map) parsed.get(0).get("partition")).get("key")).get(0));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testExcludePKArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-x", "0"))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertEquals(tool.getStdout(), 4, parsed.size());
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testTSFormatArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-t"))
+        {
+            assertThat(tool.getStdout(),
+                       CoreMatchers.both(CoreMatchers.containsString("1445008632854000"))
+                                   .and(CoreMatchers.not(CoreMatchers.containsString("2015-10-16T15:17:13.030Z"))));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testJSONLineArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-l"))
+        {
+                try
+                {
+                    mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+                    fail("Shouldn't be able to deserialize that output, now it's not a collection anymore.");
+                }
+                catch(MismatchedInputException e)
+                {
+                }
+
+                int parsedCount = 0;
+                for (String line : tool.getStdout().split("\\R"))
+                {
+                    mapper.readValue(line, Map.class);

Review comment:
       Tricky as each line is different but I added a check for one of the keys.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r487663826



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       Ok icwym now. Thx.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on pull request #704:
URL: https://github.com/apache/cassandra/pull/704#issuecomment-681971451


   Rebased
   - [CI j11](https://app.circleci.com/pipelines/github/bereng/cassandra/96/workflows/0557a488-b22b-4f60-9262-d3854eeea05b)
   - [CI j8](https://app.circleci.com/pipelines/github/bereng/cassandra/96/workflows/e040ae8d-7c79-4100-930d-a0579b48cfac)


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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r481000592



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));

Review comment:
       That was my initial solution as well. But the test class looks awful then. Even having that text in a file was fiddly. I truly feel this is the best solution, despite being almost a personal preference thing. I think the little nuisance the day this fails is assumable vs having that blob of hardcoded text. I'd like to leave it as it is if you don't mind.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r481002384



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+        Arrays.asList(Pair.of("-f", "-r"), Pair.of("--follow", "--roll_cycle")).stream().forEach(arg -> {

Review comment:
       Nice catch!




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r480998793



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));

Review comment:
       I included in the assert the stdout so we would get an equivalent useful message. But I agree `assetThat` is nices so I have changed that across the full PR where applicable.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484194204



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error

Review comment:
       icwym +1-ish. Instead of ignoring the whole test, given it's useful to test the args even if only partially imo, why don't we 'ignore' the offending assert alone? I will comment it out with an explanation and add a comment to the other Jira to make sure this gets fixed there. wdyt?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r495751454



##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable

Review comment:
       Yeah you're right. But it doesn't hurt and it's going to be there already in case we ever need it :shrug: 




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng removed a comment on pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng removed a comment on pull request #704:
URL: https://github.com/apache/cassandra/pull/704#issuecomment-670444832


   - [j11 CI](https://app.circleci.com/pipelines/github/bereng/cassandra/90/workflows/d0b98464-b356-44a6-84b5-f1121c39fa12)
   - [j8 CI](https://app.circleci.com/pipelines/github/bereng/cassandra/90/workflows/b4879b73-de29-4ee1-95f1-830e1063bba2)


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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486055568



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       I changed it but what garbage are you referring to? Like too much output?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r485079543



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+            try (ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                         path.toAbsolutePath().toString(),
+                                                                         arg.get(0),
+                                                                         arg.get(1),
+                                                                         "TEST_SECONDLY")))
+            {
+                // Tool is running in the background 'following' so we have to kill it
+                assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       > But It 'feels' a bit of an overkill imo (and this is just personal taste)
   
   Interface or not I feel is fair to talk about, the main concern is that the API is hard to use correctly and the author and reviewer needs to understand the concurrency implementations (which are only known by reading the code) at each call site.  
   
   A simple common example is the following
   
   ```
   ToolResult result = invokeTool(...);
   Assertions.assertThat(results.getCleanStderr()).isEmpty(); // may be flaky
   Assertions.assertThat(result.getExitCode()).isEqualTo(0);
   ```
   
   This code has a subtle bug (and I got bitten by it which is why I had to review the implementation to figure out what was going on) where the author needs to understand that the process may still be running and the stderr may have content if you move it *after* the exit code.  
   
   > Also the AutoClosable nature would guarantee there are not open processes left zombie.
   
   `AutoClosable` implies that we should cleanup resources, and gives warnings (though we are not that good at blocking new warnings) when resources are not cleaned up. An issue happens for every non `ToolRunner.invokeToolNoWait` method that we need to cleanup or mark ignore the closable since the contract of the API is that no resource is needed to cleanup; the advantage of different types here is that we can avoid compiler warnings and boilerplate in the 80% case (no tests so far test async-processes, and only a small set of tests in this PR test async-processes), if we leave the type `AutoClosable` then all tests should be updated as follows (that are blocking)
   
   ```
   @SuporessWarning("resource")
   ToolRunner result = invoke(...);
   ```
   
   or add unneeded boilerplate of
   
   ```
   try (ToolRunner result = invoke(...)) // try-with-resources added to avoid compiler warnings
   {
   }
   ```
   
   Given the fact that the majority of tests are working with a completed result, this adds extra boilerplate in the common case; should we not optimize for the common case?
   
   > But It 'feels' a bit of an overkill imo (and this is just personal taste)
   
   Given the above, I don't feel that it is overkill to rely on types since it would avoid compiler warnings and unneeded boilerplate (only there to avoid compiler warnings), and is explicit about how to use by blocking subtle bugs from happening.  The invoke family of methods force the author to add boilerplate to avoid compiler warnings that doesn't help the test, so don't feel that these methods solve the problem (even if CASSANDRA-16082 actually makes them blocking, they are not at the moment).
   
   I am open to other options, but feel that we need to update this API to avoid the concerns I am bring up.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484200449



##########
File path: test/unit/org/apache/cassandra/cql3/CQLTester.java
##########
@@ -451,19 +451,43 @@ public void run()
             }
         });
     }
-    
+
     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.add(jmxHost == null ? "127.0.0.1" : jmxHost);

Review comment:
       You can be testing the tool _without_ needing or wanting a server running. Like there's no need to start the server to test help messages i.e. Also you might want to test a tool's behavior when there is no reachable server, etc.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r481000764



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {

Review comment:
       Done across the full PR.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r495757970



##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);
+        }
+        catch (Exception e)
+        {
+            return new ToolResult(allArgs, -1, "", Throwables.getStackTraceAsString(e), e);
+        }
+        finally
+        {
+            System.setOut(originalSysOut);
+            System.setErr(originalSysErr);
+            System.setIn(originalSysIn);
         }
     }
 
-    private static final class StreamGobbler<T extends OutputStream> implements Runnable
+    public static Builder builder(List<String> args)
     {
-        private static final int BUFFER_SIZE = 8_192;
+        return new Builder(args);
+    }
 
-        private final InputStream input;
-        private final T out;
+    public static final class ToolResult
+    {
+        private final List<String> allArgs;
+        private final int exitCode;
+        private final String stdout;
+        private final String stderr;
+        private final Exception e;
 
-        private StreamGobbler(InputStream input, T out)
+        private ToolResult(List<String> allArgs, int exitCode, String stdout, String stderr, Exception e)
         {
-            this.input = input;
-            this.out = out;
+            this.allArgs = allArgs;
+            this.exitCode = exitCode;
+            this.stdout = stdout;
+            this.stderr = stderr;
+            this.e = e;
         }
 
-        public void run()
+        public int getExitCode()
         {
-            byte[] buffer = new byte[BUFFER_SIZE];
-            while (true)
-            {
-                try
-                {
-                    int read = input.read(buffer);
-                    if (read == -1)
-                    {
-                        return;
-                    }
-                    out.write(buffer, 0, read);
-                }
-                catch (IOException e)
-                {
-                    logger.error("Unexpected IO Error while reading stream", e);
-                    return;
-                }
-            }
+            return exitCode;
+        }
+
+        public String getStdout()
+        {
+            return stdout;
+        }
+
+        public String getStderr()
+        {
+            return stderr;
+        }
+        
+        public Exception getException()
+        {
+            return e;
         }
+        
+        /**
+         * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @return The ToolRunner instance
+         */
+        public void assertCleanStdErr()
+        {
+            assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(),
+                       getCleanedStderr().isEmpty());
+        }
+
+        public void assertOnExitCode()
+        {
+            assertExitCode(getExitCode());
+        }
+
+        private void assertExitCode(int code)
+        {
+            if (code != 0)
+                fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
+                                   argsToLogString(),
+                                   code,
+                                   getStderr(),
+                                   getStdout()));
+        }
+
+        public String argsToLogString()
+        {
+            return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output through the provided cleaners
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @param regExpCleaners
+         *            List of regExps to remove from stdErr
+         * @return The stdErr with all excludes removed
+         */
+        public String getCleanedStderr(List<String> regExpCleaners)
+        {
+            String sanitizedStderr = getStderr();
+            for (String regExp : regExpCleaners)
+                sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
+            return sanitizedStderr;
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output. Uses default list of excludes
+         * 
+         * {@link #getCleanedStderr(List)}
+         */
+        public String getCleanedStderr()
+        {
+            return getCleanedStderr(DEFAULT_CLEANERS);
+        }
+        
+        public void assertOnCleanExit()
+        {
+            assertOnExitCode();
+            assertCleanStdErr();
+        }
+    }
+
+    public interface ObservableTool extends AutoCloseable
+    {
+        String getPartialStdout();
+
+        String getPartialStderr();
+
+        boolean isDone();
+
+        ToolResult waitComplete();
+
+        @Override
+        void close();
     }
 
-    public static class Runners
+    private static final class FailedObservableTool implements ObservableTool
     {
-        public static ToolRunner invokeNodetool(String... args)
+        private final List<String> args;
+        private final IOException error;
+
+        private FailedObservableTool(IOException error, List<String> args)
+        {
+            this.args = args;
+            this.error = error;
+        }
+
+        @Override
+        public String getPartialStdout()
+        {
+            return "";
+        }
+
+        @Override
+        public String getPartialStderr()
+        {
+            return error.getMessage();
+        }
+
+        @Override
+        public boolean isDone()
         {
-            return invokeNodetool(Arrays.asList(args));
+            return true;
         }
 
-        public static ToolRunner invokeNodetool(List<String> args)
+        @Override
+        public ToolResult waitComplete()
         {
-            return invokeTool(buildNodetoolArgs(args), true);
+            return new ToolResult(args, -1, getPartialStdout(), getPartialStderr(), error);
         }
 
-        private static List<String> buildNodetoolArgs(List<String> args)
+        @Override
+        public void close()
         {
-            return CQLTester.buildNodetoolArgs(args);
+
         }
+    }
 
-        public static ToolRunner invokeClassAsTool(String... args)
+    private static final class ForkedObservableTool implements ObservableTool
+    {
+        private final CompletableFuture<Void> onComplete = new CompletableFuture<>();
+        private final ByteArrayOutputStream err = new ByteArrayOutputStream();

Review comment:
       Mine says they are unneeded, but I added them as I know you get those flagged :shrug: 




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r483335059



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+            try (ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                         path.toAbsolutePath().toString(),
+                                                                         arg.get(0),
+                                                                         arg.get(1),
+                                                                         "TEST_SECONDLY")))
+            {
+                // Tool is running in the background 'following' so we have to kill it
+                assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       I created a POC (100% ok throwing away) of this idea here https://github.com/apache/cassandra/pull/730/commits/dc4973159307fd405060c32a5f98e5b204436675.  I implemented each of the interfaces I saw and made sure running vs not running was very clear.
   
   When you get a `ToolResult` it is 100% complete, when you get a `ObservableTool` it may or not still be running
   invokeClass was changed to always return a `ToolResult` as it is a blocking operation.
   Calling the invoke family of methods never throws an exception (outside of OOM and `InterruptedException` for the blocking calls) and instead translates these into error codes that you can look get a `ToolResult` on; example case is calling a command that doesn't exist, the `ToolResult` will have this in the stderr rather than throw an exception




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r495760281



##########
File path: test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
##########
@@ -75,20 +73,20 @@ public void testBulkLoader_WithArgs() throws Exception
     @Test
     public void testBulkLoader_WithArgs1() throws Exception
     {
-        try
-        {
-            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)
-        {
-            if (!(e.getCause() instanceof BulkLoadException))
-                throw e;
-            if (!(e.getCause().getCause() instanceof NoHostAvailableException))
-                throw e;
-        }
-        assertNoUnexpectedThreadsStarted(null, new String[]{"globalEventExecutor-1-1", "globalEventExecutor-1-2"});
+        ToolResult tool = ToolRunner.invokeClass(BulkLoader.class,
+                                                 "-d",
+                                                 "127.9.9.1",
+                                                 "--port",
+                                                 "9042",
+                                                 OfflineToolUtils.sstableDirName("legacy_sstables", "legacy_ma_simple"));
+
+        assertEquals(-1, tool.getExitCode());
+        if (!(tool.getException().getCause() instanceof BulkLoadException))

Review comment:
       Mmmm they don't show up for me...




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r483118732



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))

Review comment:
       do we need to close? test blocks on the exit, so the force delete process doesn't seem needed?

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));

Review comment:
       For some reason https://github.com/apache/cassandra/pull/704#discussion_r480362270 got lost so adding back...
   
   Even though it is more verbose I am worried about maintenance.  If someone adds a new argument or a new command then they get an error that isn't actionable, they then need to read the test, understand what is going on, then regenerate a new hash; it is easier for the author to make changes to the expected string.

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error

Review comment:
       If you are planning to fix the jdk11 bug in a different ticket, can we move these tests to those tickets?  We can also leave it without the WARNING exclusion and mark the test as ignored (such as `org.apache.cassandra.distributed.test.RepairCoordinatorFailingMessageTest`).

##########
File path: test/unit/org/apache/cassandra/cql3/CQLTester.java
##########
@@ -451,19 +451,43 @@ public void run()
             }
         });
     }
-    
+
     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.add(jmxHost == null ? "127.0.0.1" : jmxHost);

Review comment:
       if null should we fail?  this is defined if `jmxServer = JMXServerUtils.createJMXServer(jmxPort, true);` is setup and running, so if not present then doesn't this mean we didn't start the xmx server?

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());

Review comment:
       should we use `tool.assertOnExitCode()` as it can provide a better message?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484197106



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());

Review comment:
       +1




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r485079946



##########
File path: test/unit/org/apache/cassandra/tools/ClearSnapshotTest.java
##########
@@ -54,32 +56,43 @@ public static void teardown() throws IOException
     @Test
     public void testClearSnapshot_NoArgs() throws IOException
     {
-        ToolRunner tool = runner.invokeNodetool("clearsnapshot");
-        assertEquals(2, tool.getExitCode());
-        assertTrue("Tool stderr: " +  tool.getStderr(), tool.getStderr().contains("Specify snapshot name or --all"));
+        try (ToolRunner tool = runner.invokeNodetool("clearsnapshot"))

Review comment:
       yep, lets wait till we agree on the plan




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484197967



##########
File path: test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
##########
@@ -36,9 +36,11 @@
     @Test
     public void testBulkLoader_NoArgs() throws Exception
     {
-        ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader");
-        assertEquals(1, tool.getExitCode());
-        assertTrue(!tool.getStderr().isEmpty());
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader"))
+        {
+            assertEquals(1, tool.getExitCode());
+            assertTrue(!tool.getCleanedStderr().isEmpty());

Review comment:
       +1000




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486060433



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
##########
@@ -52,5 +67,101 @@ public void testStandaloneSplitter_NoArgs()
         assertServerNotLoaded();
     }
 
-    // Note: StandaloneSplitter modifies sstables
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "-h"))
+        {
+            String help = "usage: sstablessplit [options] <filename> [<filename>]*\n" + 
+                          "--\n" + 
+                          "Split the provided sstables files in sstables of maximum provided file\n" + 
+                          "size (see option --size).\n" + 
+                          "--\n" + 
+                          "Options are:\n" + 
+                          "    --debug         display stack traces\n" + 
+                          " -h,--help          display this help message\n" + 
+                          "    --no-snapshot   don't snapshot the sstables before splitting\n" + 
+                          " -s,--size <size>   maximum size in MB for the output sstables (default:\n" + 
+                          "                    50)\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "--debugwrong", "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testWrongFilename()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+            assertEquals(1,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "--no-snapshot").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), arg, "mockFile"))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                assertEquals(1,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testSizeArg()
+    {
+        Arrays.asList(Pair.of("-s", ""), Pair.of("-s", "w"), Pair.of("--size", ""), Pair.of("--size", "w"))
+              .forEach(arg -> {
+                  try
+                  {
+                      runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
+                                               arg.getLeft(),
+                                               arg.getRight(),
+                                               "mockFile");
+                      fail("Shouldn't be able to parse wrong input as number");
+                  }
+                  catch(RuntimeException e)

Review comment:
       Given it's a nit, I can't 'bulk replace it' bc it needs manual edit on each file and that it's consistent with the rest of unit tests I'll leave as it is.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r487663297



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();

Review comment:
       I'm on Eclipse. Yes I know, I know... Still I tried `ant eclipse-warnings` and nothing pops up.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r481611633



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));

Review comment:
       This in the middle of the test class looks awful imo:
   
   `@Test
       public void testMaybeChangeDocs()
       {
           // If you added, modified options or help, please update docs if necessary
           ToolRunner tool = runner.invokeTool(toolPath, "-h");
           assertEquals(tool.getStdout(), "usage: nodetool [(-u <username> | --username <username>)]
           [(-pw <password> | --password <password>)] [(-h <host> | --host <host>)]
           [(-p <port> | --port <port>)]
           [(-pwf <passwordFilePath> | --password-file <passwordFilePath>)]
           [(-pp | --print-port)] <command> [<args>]
   
   The most commonly used nodetool commands are:
       assassinate                  Forcefully remove a dead node without re-replicating any data.  Use as a last resort if you cannot removenode
       bootstrap                    Monitor/manage node's bootstrap process
       cleanup                      Triggers the immediate cleanup of keys no longer belonging to a node. By default, clean all keyspaces
       clearsnapshot                Remove the snapshot with the given name from the given keyspaces. If no snapshotName is specified we will remove all snapshots
       clientstats                  Print information about connected clients
       compact                      Force a (major) compaction on one or more tables or user-defined compaction on given SSTables
       compactionhistory            Print history of compaction
       compactionstats              Print statistics on compactions
       decommission                 Decommission the *node I am connecting to*
       describecluster              Print the name, snitch, partitioner and schema version of a cluster
       describering                 Shows the token ranges info of a given keyspace
       disableauditlog              Disable the audit log
       disableautocompaction        Disable autocompaction for the given keyspace and table
       disablebackup                Disable incremental backup
       disablebinary                Disable native transport (binary protocol)
       disablefullquerylog          Disable the full query log
       disablegossip                Disable gossip (effectively marking the node down)
       disablehandoff               Disable storing hinted handoffs
       disablehintsfordc            Disable hints for a data center
       disableoldprotocolversions   Disable old protocol versions
       drain                        Drain the node (stop accepting writes and flush all tables)
       enableauditlog               Enable the audit log
       enableautocompaction         Enable autocompaction for the given keyspace and table
       enablebackup                 Enable incremental backup
       enablebinary                 Reenable native transport (binary protocol)
       enablefullquerylog           Enable full query logging, defaults for the options are configured in cassandra.yaml
       enablegossip                 Reenable gossip
       enablehandoff                Reenable future hints storing on the current node
       enablehintsfordc             Enable hints for a data center that was previsouly disabled
       enableoldprotocolversions    Enable old protocol versions
       failuredetector              Shows the failure detector information for the cluster
       flush                        Flush one or more tables
       garbagecollect               Remove deleted data from one or more tables
       gcstats                      Print GC Statistics
       getbatchlogreplaythrottle    Print batchlog replay throttle in KB/s. This is reduced proportionally to the number of nodes in the cluster.
       getcompactionthreshold       Print min and max compaction thresholds for a given table
       getcompactionthroughput      Print the MB/s throughput cap for compaction in the system
       getconcurrency               Get maximum concurrency for processing stages
       getconcurrentcompactors      Get the number of concurrent compactors in the system.
       getconcurrentviewbuilders    Get the number of concurrent view builders in the system
       getendpoints                 Print the end points that owns the key
       getinterdcstreamthroughput   Print the Mb/s throughput cap for inter-datacenter streaming in the system
       getlogginglevels             Get the runtime logging levels
       getmaxhintwindow             Print the max hint window in ms
       getseeds                     Get the currently in use seed node IP list excluding the node IP
       getsstables                  Print the sstable filenames that own the key
       getstreamthroughput          Print the Mb/s throughput cap for streaming in the system
       gettimeout                   Print the timeout of the given type in ms
       gettraceprobability          Print the current trace probability value
       gossipinfo                   Shows the gossip information for the cluster
       help                         Display help information
       import                       Import new SSTables to the system
       info                         Print node information (uptime, load, ...)
       invalidatecountercache       Invalidate the counter cache
       invalidatekeycache           Invalidate the key cache
       invalidaterowcache           Invalidate the row cache
       join                         Join the ring
       listsnapshots                Lists all the snapshots along with the size on disk and true size. True size is the total size of all SSTables which are not backed up to disk. Size on disk is total size of the snapshot on disk. Total TrueDiskSpaceUsed does not make any SSTable deduplication.
       move                         Move node on the token ring to a new token
       netstats                     Print network information on provided host (connecting node by default)
       pausehandoff                 Pause hints delivery process
       profileload                  Low footprint profiling of activity for a period of time
       proxyhistograms              Print statistic histograms for network operations
       rangekeysample               Shows the sampled keys held across all keyspaces
       rebuild                      Rebuild data by streaming from other nodes (similarly to bootstrap)
       rebuild_index                A full rebuild of native secondary indexes for a given table
       refresh                      Load newly placed SSTables to the system without restart
       refreshsizeestimates         Refresh system.size_estimates
       reloadlocalschema            Reload local node schema from system tables
       reloadseeds                  Reload the seed node list from the seed node provider
       reloadssl                    Signals Cassandra to reload SSL certificates
       reloadtriggers               Reload trigger classes
       relocatesstables             Relocates sstables to the correct disk
       removenode                   Show status of current node removal, force completion of pending removal or remove provided ID
       repair                       Repair one or more tables
       repair_admin                 list and fail incremental repair sessions
       replaybatchlog               Kick off batchlog replay and wait for finish
       resetfullquerylog            Stop the full query log and clean files in the configured full query log directory from cassandra.yaml as well as JMX
       resetlocalschema             Reset node's local schema and resync
       resumehandoff                Resume hints delivery process
       ring                         Print information about the token ring
       scrub                        Scrub (rebuild sstables for) one or more tables
       setbatchlogreplaythrottle    Set batchlog replay throttle in KB per second, or 0 to disable throttling. This will be reduced proportionally to the number of nodes in the cluster.
       setcachecapacity             Set global key, row, and counter cache capacities (in MB units)
       setcachekeystosave           Set number of keys saved by each cache for faster post-restart warmup. 0 to disable
       setcompactionthreshold       Set min and max compaction thresholds for a given table
       setcompactionthroughput      Set the MB/s throughput cap for compaction in the system, or 0 to disable throttling
       setconcurrency               Set maximum concurrency for processing stage
       setconcurrentcompactors      Set number of concurrent compactors in the system.
       setconcurrentviewbuilders    Set the number of concurrent view builders in the system
       sethintedhandoffthrottlekb   Set hinted handoff throttle in kb per second, per delivery thread.
       setinterdcstreamthroughput   Set the Mb/s throughput cap for inter-datacenter streaming in the system, or 0 to disable throttling
       setlogginglevel              Set the log level threshold for a given component or class. Will reset to the initial configuration if called with no parameters.
       setmaxhintwindow             Set the specified max hint window in ms
       setstreamthroughput          Set the Mb/s throughput cap for streaming in the system, or 0 to disable throttling
       settimeout                   Set the specified timeout in ms, or 0 to disable timeout
       settraceprobability          Sets the probability for tracing any given request to value. 0 disables, 1 enables for all requests, 0 is the default
       sjk                          Run commands of 'Swiss Java Knife'. Run 'nodetool sjk --help' for more information.
       snapshot                     Take a snapshot of specified keyspaces or a snapshot of the specified table
       status                       Print cluster information (state, load, IDs, ...)
       statusautocompaction         status of autocompaction of the given keyspace and table
       statusbackup                 Status of incremental backup
       statusbinary                 Status of native transport (binary protocol)
       statusgossip                 Status of gossip
       statushandoff                Status of storing future hints on the current node
       stop                         Stop compaction
       stopdaemon                   Stop cassandra daemon
       tablehistograms              Print statistic histograms for a given table
       tablestats                   Print statistics on tables
       toppartitions                Sample and print the most active partitions
       tpstats                      Print usage statistics of thread pools
       truncatehints                Truncate all hints on the local node, or truncate hints for the endpoint(s) specified.
       upgradesstables              Rewrite sstables (for the requested tables) that are not on the current version (thus upgrading them to said current version)
       verify                       Verify (check data checksum for) one or more tables
       version                      Print cassandra version
       viewbuildstatus              Show progress of a materialized view build
   
   See 'nodetool help <command>' for more information on a specific command.
   ");
   }`




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] driftx commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
driftx commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r481392461



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));

Review comment:
       I don't understand how, given that the plaintext and md5 equate 1:1, using the text was fiddly?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484265688



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+            try (ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                         path.toAbsolutePath().toString(),
+                                                                         arg.get(0),
+                                                                         arg.get(1),
+                                                                         "TEST_SECONDLY")))
+            {
+                // Tool is running in the background 'following' so we have to kill it
+                assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       First of all let me thank you for taking the time to put up such a detailed POC. I understand the process 'completed/in-flight' difference and how that is not crystal clear in the API, it's up to user to know what he's doing atm. I didn't think about that, thx for raising the point.
   
   Having interfaces clearly guarding each of those scenarios brings obvious benefits and I am not against it. But It 'feels' a bit of an overkill imo (and this is just personal taste). So I think we can achieve similar results along your train of thought in a simpler way if:
   - `ToolRunner.waitFor()` calls `foceKill()`. That way all `invoke()` methods family would return a completed result always.
   - Only `ToolRunner.invokeToolNoWait()` wouldn't. And it is quite obvious given the name of the method, imo, that it's the user's responsibility to handle that scenario properly.
   - Also the `AutoClosable` nature would guarantee there are not open processes left zombie.
   
   Although your concern is real and it itches me now. So I would propose on top of that leaving all `invoke()` methods as they are but rename `invokeToolNoWait()` to `invokeToolNoWaitCompletion()`.
   
   Iiuc at the end of the day we're saying about the same. You're suggesting making that concern more explicit with interfaces & others while I am thinking in more simplistic terms. wdyt did I miss anything?
   
   Thanks again for the thorough review.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on pull request #704:
URL: https://github.com/apache/cassandra/pull/704#issuecomment-670444832


   - [j11 CI](https://app.circleci.com/pipelines/github/bereng/cassandra/90/workflows/d0b98464-b356-44a6-84b5-f1121c39fa12)
   - [j8 CI](https://app.circleci.com/pipelines/github/bereng/cassandra/90/workflows/b4879b73-de29-4ee1-95f1-830e1063bba2)


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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on pull request #704:
URL: https://github.com/apache/cassandra/pull/704#issuecomment-697198333


   @dcapwell I moved the PR to use your with/out forking API as promised and rebased to pull in the latest `ToolRunner` new uses.


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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng closed pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng closed pull request #704:
URL: https://github.com/apache/cassandra/pull/704


   


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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486062150



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
##########
@@ -52,5 +67,101 @@ public void testStandaloneSplitter_NoArgs()
         assertServerNotLoaded();
     }
 
-    // Note: StandaloneSplitter modifies sstables
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "-h"))
+        {
+            String help = "usage: sstablessplit [options] <filename> [<filename>]*\n" + 
+                          "--\n" + 
+                          "Split the provided sstables files in sstables of maximum provided file\n" + 
+                          "size (see option --size).\n" + 
+                          "--\n" + 
+                          "Options are:\n" + 
+                          "    --debug         display stack traces\n" + 
+                          " -h,--help          display this help message\n" + 
+                          "    --no-snapshot   don't snapshot the sstables before splitting\n" + 
+                          " -s,--size <size>   maximum size in MB for the output sstables (default:\n" + 
+                          "                    50)\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "--debugwrong", "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testWrongFilename()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+            assertEquals(1,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "--no-snapshot").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), arg, "mockFile"))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                assertEquals(1,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testSizeArg()
+    {
+        Arrays.asList(Pair.of("-s", ""), Pair.of("-s", "w"), Pair.of("--size", ""), Pair.of("--size", "w"))
+              .forEach(arg -> {
+                  try
+                  {
+                      runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
+                                               arg.getLeft(),
+                                               arg.getRight(),
+                                               "mockFile");
+                      fail("Shouldn't be able to parse wrong input as number");
+                  }
+                  catch(RuntimeException e)
+                  {
+                      if (!(e.getCause() instanceof NumberFormatException))
+                          fail("Should have failed parsing a non-num.");
+                  }
+              });
+
+        Arrays.asList(Pair.of("-s", "0"),
+                      Pair.of("-s", "1000"),
+                      Pair.of("-s", "-1"),
+                      Pair.of("--size", "0"),
+                      Pair.of("--size", "1000"),
+                      Pair.of("--size", "-1"))
+              .forEach(arg -> {
+                  try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
+                                                                  arg.getLeft(),
+                                                                  arg.getRight(),
+                                                                  "mockFile"))
+                  {
+                      assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                      assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                      assertEquals(1,tool.getExitCode());
+                  }
+                  assertCorrectEnvPostTest();

Review comment:
       More tests will be added when we tackle the specific ticket to add proper testing for the specific tool. Those will be _appended_ (notice `OrderedJUnit4ClassRunner`) and depending on what the tool does/loads the post test env can be different. So `assertCorrectEnvPostTest` only applies on a case by case basis despite for args testing it applies always.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r485920716



##########
File path: test/unit/org/apache/cassandra/tools/cqlsh/CqlshTest.java
##########
@@ -0,0 +1,53 @@
+/*
+ * 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.cqlsh;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+import org.hamcrest.CoreMatchers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class CqlshTest extends CQLTester
+{
+    private ToolRunner.Runners runner = new ToolRunner.Runners();

Review comment:
       make final

##########
File path: test/unit/org/apache/cassandra/tools/cqlsh/CqlshTest.java
##########
@@ -0,0 +1,53 @@
+/*
+ * 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.cqlsh;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+import org.hamcrest.CoreMatchers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class CqlshTest extends CQLTester
+{
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
+    @BeforeClass
+    public static void setUp()
+    {
+        requireNetwork();
+    }
+
+    @Test
+    public void testKeyspaceRequired() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeCqlsh("SELECT * FROM test"))
+        {
+        assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No keyspace has been specified"));

Review comment:
       can you fix formatting?

##########
File path: test/unit/org/apache/cassandra/tools/ToolsEnvsConfigsTest.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.util.ArrayList;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class ToolsEnvsConfigsTest
+{
+    //Some JDK can output env info on stdout/err. Check we can clean them
+    @Test
+    public void testJDKEnvInfoDefaultCleaners()
+    {
+        ToolRunner runner = new ToolRunner(CQLTester.buildNodetoolArgs(new ArrayList<String>()), true);

Review comment:
       should use try-with-resources, any exception will cause this to not properly close.

##########
File path: test/unit/org/apache/cassandra/tools/ToolsEnvsConfigsTest.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.util.ArrayList;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class ToolsEnvsConfigsTest
+{
+    //Some JDK can output env info on stdout/err. Check we can clean them
+    @Test
+    public void testJDKEnvInfoDefaultCleaners()
+    {
+        ToolRunner runner = new ToolRunner(CQLTester.buildNodetoolArgs(new ArrayList<String>()), true);
+        runner = runner.withEnvs(ImmutableMap.of("_JAVA_OPTIONS", "-Djava.net.preferIPv4Stack=true"));

Review comment:
       since withEnvs returns an AutoCloseable class this causes a compiler warning as resources are not managed, can you fix the warning?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testQuickArg()
+    {
+        Arrays.asList("-q", "--quick").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("quick=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();

Review comment:
       this adds a compiler warning as this logic doesn't manage the returned resource, can you fix the compiler warnings?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();

Review comment:
       this adds a compiler warning as this logic doesn't manage the returned resource, can you fix the compiler warnings?

##########
File path: test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java
##########
@@ -43,10 +56,73 @@ public void testSSTableRepairedAtSetter_NoArgs()
     }
 
     @Test
-    public void testSSTableRepairedAtSetter_WithArgs() throws Exception
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableRepairedAtSetter.class.getName(), "-h"))
+        {
+            String help = "This command should be run with Cassandra stopped, otherwise you will get very strange behavior\n" + 
+                          "Verify that Cassandra is not running and then execute the command like this:\n" + 
+                          "Usage: sstablerepairedset --really-set [--is-repaired | --is-unrepaired] [-f <sstable-list> | <sstables>]\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableRepairedAtSetter.class.getName(),
+                                                       "--debugwrong",
+                                                       findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testIsrepairedArg() throws Exception
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableRepairedAtSetter", "--really-set", "--is-repaired", findOneSSTable("legacy_sstables", "legacy_ma_simple"))
-              .waitAndAssertOnCleanExit();
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableRepairedAtSetter.class.getName(),
+                                                       "--really-set",
+                                                       "--is-repaired",
+                                                       findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            tool.waitAndAssertOnCleanExit();
+        }
+        assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testIsunrepairedArg() throws Exception
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableRepairedAtSetter.class.getName(), "--really-set", "--is-unrepaired", findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+              tool.waitAndAssertOnCleanExit();
+        }
+        assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaNotLoaded();
+        assertCLSMNotLoaded();
+        assertSystemKSNotLoaded();
+        assertKeyspaceNotLoaded();
+        assertServerNotLoaded();
+    }
+
+    @Test
+    public void testFilesArg() throws Exception
+    {
+        String file = Files.write(Paths.get("./sstablelist.txt"), findOneSSTable("legacy_sstables", "legacy_ma_simple").getBytes()).getFileName().toAbsolutePath().toString();

Review comment:
       shouldn't write to the current path, nothing will clean this up; should stick to build directory or tmp.

##########
File path: test/unit/org/apache/cassandra/tools/SSTableExportTest.java
##########
@@ -43,12 +62,146 @@ public void testSSTableExport_NoArgs()
     }
 
     @Test
-    public void testSSTableExport_WithArgs() throws Exception
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableExport.class.getName()))
+        {
+            String help = "usage: sstabledump <sstable file path> <options>\n" + 
+                           "                   \n" + 
+                           "Dump contents of given SSTable to standard output in JSON format.\n" + 
+                           " -d         CQL row per line internal representation\n" + 
+                           " -e         enumerate partition keys only\n" + 
+                           " -k <arg>   Partition key\n" + 
+                           " -l         Output json lines, by partition\n" + 
+                           " -t         Print raw timestamps instead of iso8601 date strings\n" + 
+                           " -x <arg>   Excluded partition key\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableExport.class.getName(), "--debugwrong", findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport",findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertTrue(tool.getStdout(), parsed.get(0).get("partition") != null);
+            assertTrue(tool.getStdout(), parsed.get(0).get("rows") != null);
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testCQLRowArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-d"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.startsWith("[0]"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testPKOnlyArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-e"))
+        {
+            assertEquals(tool.getStdout(), "[ [ \"0\" ], [ \"1\" ], [ \"2\" ], [ \"3\" ], [ \"4\" ]\n]", tool.getStdout());
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testPKArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-k", "0"))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertEquals(tool.getStdout(), 1, parsed.size());
+            assertEquals(tool.getStdout(), "0", ((List) ((Map) parsed.get(0).get("partition")).get("key")).get(0));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testExcludePKArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-x", "0"))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertEquals(tool.getStdout(), 4, parsed.size());
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testTSFormatArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-t"))
+        {
+            assertThat(tool.getStdout(),
+                       CoreMatchers.both(CoreMatchers.containsString("1445008632854000"))
+                                   .and(CoreMatchers.not(CoreMatchers.containsString("2015-10-16T15:17:13.030Z"))));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testJSONLineArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-l"))
+        {
+                try
+                {
+                    mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+                    fail("Shouldn't be able to deserialize that output, now it's not a collection anymore.");
+                }
+                catch(MismatchedInputException e)
+                {
+                }
+
+                int parsedCount = 0;
+                for (String line : tool.getStdout().split("\\R"))
+                {
+                    mapper.readValue(line, Map.class);

Review comment:
       this only validates that this is a object, should also validate the content.

##########
File path: test/unit/org/apache/cassandra/tools/cassandrastress/CassandrastressTest.java
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.cassandrastress;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+import org.hamcrest.CoreMatchers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class CassandrastressTest extends CQLTester
+{
+    private ToolRunner.Runners runner = new ToolRunner.Runners();

Review comment:
       make final

##########
File path: test/unit/org/apache/cassandra/tools/cqlsh/CqlshTest.java
##########
@@ -0,0 +1,53 @@
+/*
+ * 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.cqlsh;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+import org.hamcrest.CoreMatchers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+public class CqlshTest extends CQLTester
+{
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
+    @BeforeClass
+    public static void setUp()
+    {
+        requireNetwork();
+    }
+
+    @Test
+    public void testKeyspaceRequired() throws IOException

Review comment:
       exception not thrown, can you remove

##########
File path: test/unit/org/apache/cassandra/tools/ToolsEnvsConfigsTest.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.util.ArrayList;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class ToolsEnvsConfigsTest
+{
+    //Some JDK can output env info on stdout/err. Check we can clean them
+    @Test
+    public void testJDKEnvInfoDefaultCleaners()
+    {
+        ToolRunner runner = new ToolRunner(CQLTester.buildNodetoolArgs(new ArrayList<String>()), true);
+        runner = runner.withEnvs(ImmutableMap.of("_JAVA_OPTIONS", "-Djava.net.preferIPv4Stack=true"));
+        runner = runner.start();

Review comment:
       since start returns an `AutoCloseable` class this causes a compiler warning as resources are not managed, can you fix the warning?

##########
File path: test/unit/org/apache/cassandra/tools/cassandrastress/CassandrastressTest.java
##########
@@ -0,0 +1,55 @@
+/*
+ * 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.cassandrastress;
+
+import java.io.IOException;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.tools.ToolRunner;
+import org.hamcrest.CoreMatchers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class CassandrastressTest extends CQLTester
+{
+    private ToolRunner.Runners runner = new ToolRunner.Runners();
+    
+    @BeforeClass
+    public static void setUp()
+    {
+        requireNetwork();
+    }
+
+    @Test
+    public void testNoArgsPrintsHelp() throws IOException

Review comment:
       code doesn't throw, can you remove the throws

##########
File path: test/unit/org/apache/cassandra/tools/ToolsEnvsConfigsTest.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.util.ArrayList;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(OrderedJUnit4ClassRunner.class)

Review comment:
       since this is only a single test, is this needed?

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -459,14 +480,24 @@ public ToolRunner invokeTool(String... args)
 
         public ToolRunner invokeTool(List<String> args)
         {
-            return invokeTool(args, true);
+            return invokeTool(args, true, true);
         }
 
-        public ToolRunner invokeTool(List<String> args, boolean runOutOfProcess)
+        public ToolRunner invokeToolNoWait(List<String> args)
+        {
+            return invokeTool(args, true, false);
+        }
+
+        public ToolRunner invokeTool(List<String> args, boolean runOutOfProcess, boolean wait)
         {
             ToolRunner runner = new ToolRunner(args, runOutOfProcess);
-            runner.start().waitFor();
+            if (wait)
+                runner.start().waitFor();

Review comment:
       both calls to `start()` cause a compiler warning since the resource isn't managed, can you fix the warning?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -18,22 +18,33 @@
 
 package org.apache.cassandra.tools;
 
+import java.util.Arrays;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
+import org.assertj.core.api.Assertions;
+import org.hamcrest.CoreMatchers;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 @RunWith(OrderedJUnit4ClassRunner.class)
 public class StandaloneVerifierTest extends OfflineToolUtils
 {
     private ToolRunner.Runners runner = new ToolRunner.Runners();

Review comment:
       final

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       this produces a lot of garbage, it would be best to call once rather than twice for the message.  `Assertions.assertThat(tool.getCleanedStderr()).isEmpty()` would fix this

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       this produces a lot of garbage, it would be best to call once rather than twice for the message.  `Assertions.assertThat(tool.getCleanedStderr()).isEmpty()` would fix this

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testQuickArg()
+    {
+        Arrays.asList("-q", "--quick").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("quick=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testRepairStatusArg()
+    {
+        Arrays.asList("-r", "--mutate_repair_status").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("mutateRepairStatus=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();

Review comment:
       this adds a compiler warning as this logic doesn't manage the returned resource, can you fix the compiler warnings?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testQuickArg()
+    {
+        Arrays.asList("-q", "--quick").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("quick=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testRepairStatusArg()
+    {
+        Arrays.asList("-r", "--mutate_repair_status").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("mutateRepairStatus=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testVerboseArg()
+    {
+        Arrays.asList("-v", "--verbose").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("verbose=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();

Review comment:
       this adds a compiler warning as this logic doesn't manage the returned resource, can you fix the compiler warnings?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testQuickArg()
+    {
+        Arrays.asList("-q", "--quick").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("quick=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       this produces a lot of garbage, it would be best to call once rather than twice for the message.  `Assertions.assertThat(tool.getCleanedStderr()).isEmpty()` would fix this

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();

Review comment:
       this adds a compiler warning as this logic doesn't manage the returned resource, can you fix the compiler warnings?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testQuickArg()
+    {
+        Arrays.asList("-q", "--quick").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("quick=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testRepairStatusArg()
+    {
+        Arrays.asList("-r", "--mutate_repair_status").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("mutateRepairStatus=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       this produces a lot of garbage, it would be best to call once rather than twice for the message.  `Assertions.assertThat(tool.getCleanedStderr()).isEmpty()` would fix this

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testQuickArg()
+    {
+        Arrays.asList("-q", "--quick").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("quick=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testRepairStatusArg()
+    {
+        Arrays.asList("-r", "--mutate_repair_status").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("mutateRepairStatus=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testVerboseArg()
+    {
+        Arrays.asList("-v", "--verbose").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("verbose=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       this produces a lot of garbage, it would be best to call once rather than twice for the message.  `Assertions.assertThat(tool.getCleanedStderr()).isEmpty()` would fix this

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testQuickArg()
+    {
+        Arrays.asList("-q", "--quick").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("quick=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testRepairStatusArg()
+    {
+        Arrays.asList("-r", "--mutate_repair_status").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("mutateRepairStatus=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       this produces a lot of garbage, it would be best to call once rather than twice for the message.  `Assertions.assertThat(tool.getCleanedStderr()).isEmpty()` would fix this

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
##########
@@ -43,12 +54,81 @@ public void testStandaloneUpgrader_NoArgs()
     }
 
     @Test
-    public void testStandaloneUpgrader_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneUpgrader", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "-h"))
+        {
+            String help = "usage: sstableupgrade [options] <keyspace> <cf> [snapshot]\n" + 
+                           "--\n" + 
+                           "Upgrade the sstables in the given cf (or snapshot) to the current version\n" + 
+                           "of Cassandra.This operation will rewrite the sstables in the specified cf\n" + 
+                           "to match the currently installed version of Cassandra.\n" + 
+                           "The snapshot option will only upgrade the specified snapshot. Upgrading\n" + 
+                           "snapshots is required before attempting to restore a snapshot taken in a\n" + 
+                           "major version older than the major version Cassandra is currently running.\n" + 
+                           "This will replace the files in the given snapshot as well as break any\n" + 
+                           "hard links to live sstables.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           "    --debug         display stack traces\n" + 
+                           " -h,--help          display this help message\n" + 
+                           " -k,--keep-source   do not delete the source sstables\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "system_schema", "tables"))
+        {
+            assertTrue(tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "-k", "--keep-source").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertTrue("Arg: [" + arg + "]\n" + tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));
+                assertTrue("Arg: [" + arg + "]\n" + tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                assertEquals(0,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), arg))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue("Arg: [" + arg + "]\n" + tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();

Review comment:
       this adds a compiler warning as this logic doesn't manage the returned resource, can you fix the compiler warnings?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       this produces a lot of garbage, it would be best to call once rather than twice for the message.  `Assertions.assertThat(tool.getCleanedStderr()).isEmpty()` would fix this

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
##########
@@ -52,5 +67,101 @@ public void testStandaloneSplitter_NoArgs()
         assertServerNotLoaded();
     }
 
-    // Note: StandaloneSplitter modifies sstables
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "-h"))
+        {
+            String help = "usage: sstablessplit [options] <filename> [<filename>]*\n" + 
+                          "--\n" + 
+                          "Split the provided sstables files in sstables of maximum provided file\n" + 
+                          "size (see option --size).\n" + 
+                          "--\n" + 
+                          "Options are:\n" + 
+                          "    --debug         display stack traces\n" + 
+                          " -h,--help          display this help message\n" + 
+                          "    --no-snapshot   don't snapshot the sstables before splitting\n" + 
+                          " -s,--size <size>   maximum size in MB for the output sstables (default:\n" + 
+                          "                    50)\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "--debugwrong", "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testWrongFilename()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+            assertEquals(1,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "--no-snapshot").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), arg, "mockFile"))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                assertEquals(1,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testSizeArg()
+    {
+        Arrays.asList(Pair.of("-s", ""), Pair.of("-s", "w"), Pair.of("--size", ""), Pair.of("--size", "w"))
+              .forEach(arg -> {
+                  try
+                  {
+                      runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
+                                               arg.getLeft(),
+                                               arg.getRight(),
+                                               "mockFile");
+                      fail("Shouldn't be able to parse wrong input as number");
+                  }
+                  catch(RuntimeException e)
+                  {
+                      if (!(e.getCause() instanceof NumberFormatException))
+                          fail("Should have failed parsing a non-num.");
+                  }
+              });
+
+        Arrays.asList(Pair.of("-s", "0"),
+                      Pair.of("-s", "1000"),
+                      Pair.of("-s", "-1"),
+                      Pair.of("--size", "0"),
+                      Pair.of("--size", "1000"),
+                      Pair.of("--size", "-1"))
+              .forEach(arg -> {
+                  try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
+                                                                  arg.getLeft(),
+                                                                  arg.getRight(),
+                                                                  "mockFile"))
+                  {
+                      assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                      assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                      assertEquals(1,tool.getExitCode());
+                  }
+                  assertCorrectEnvPostTest();

Review comment:
       general comment, any reason this isn't an `@After` method?  that way you don't need to add constantly?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
##########
@@ -43,12 +54,81 @@ public void testStandaloneUpgrader_NoArgs()
     }
 
     @Test
-    public void testStandaloneUpgrader_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneUpgrader", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "-h"))
+        {
+            String help = "usage: sstableupgrade [options] <keyspace> <cf> [snapshot]\n" + 
+                           "--\n" + 
+                           "Upgrade the sstables in the given cf (or snapshot) to the current version\n" + 
+                           "of Cassandra.This operation will rewrite the sstables in the specified cf\n" + 
+                           "to match the currently installed version of Cassandra.\n" + 
+                           "The snapshot option will only upgrade the specified snapshot. Upgrading\n" + 
+                           "snapshots is required before attempting to restore a snapshot taken in a\n" + 
+                           "major version older than the major version Cassandra is currently running.\n" + 
+                           "This will replace the files in the given snapshot as well as break any\n" + 
+                           "hard links to live sstables.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           "    --debug         display stack traces\n" + 
+                           " -h,--help          display this help message\n" + 
+                           " -k,--keep-source   do not delete the source sstables\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "system_schema", "tables"))
+        {
+            assertTrue(tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "-k", "--keep-source").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertTrue("Arg: [" + arg + "]\n" + tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));
+                assertTrue("Arg: [" + arg + "]\n" + tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                assertEquals(0,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), arg))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue("Arg: [" + arg + "]\n" + tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       Should use something like `Assertions.assertThat(tool.getCleanedStderr()).as("Arg: [%s]", arg).isEmpty();` will avoid the garbage and have similar error message.

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
##########
@@ -43,12 +54,81 @@ public void testStandaloneUpgrader_NoArgs()
     }
 
     @Test
-    public void testStandaloneUpgrader_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneUpgrader", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "-h"))
+        {
+            String help = "usage: sstableupgrade [options] <keyspace> <cf> [snapshot]\n" + 
+                           "--\n" + 
+                           "Upgrade the sstables in the given cf (or snapshot) to the current version\n" + 
+                           "of Cassandra.This operation will rewrite the sstables in the specified cf\n" + 
+                           "to match the currently installed version of Cassandra.\n" + 
+                           "The snapshot option will only upgrade the specified snapshot. Upgrading\n" + 
+                           "snapshots is required before attempting to restore a snapshot taken in a\n" + 
+                           "major version older than the major version Cassandra is currently running.\n" + 
+                           "This will replace the files in the given snapshot as well as break any\n" + 
+                           "hard links to live sstables.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           "    --debug         display stack traces\n" + 
+                           " -h,--help          display this help message\n" + 
+                           " -k,--keep-source   do not delete the source sstables\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "system_schema", "tables"))
+        {
+            assertTrue(tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "-k", "--keep-source").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertTrue("Arg: [" + arg + "]\n" + tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));

Review comment:
       can simplify to something like `Assertions.assertThat(tool.getStdout()).as("Arg: [%s]", arg).isEqualTo("Found 0 sstables that need upgrading.\n");`

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
##########
@@ -43,12 +54,81 @@ public void testStandaloneUpgrader_NoArgs()
     }
 
     @Test
-    public void testStandaloneUpgrader_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneUpgrader", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "-h"))
+        {
+            String help = "usage: sstableupgrade [options] <keyspace> <cf> [snapshot]\n" + 
+                           "--\n" + 
+                           "Upgrade the sstables in the given cf (or snapshot) to the current version\n" + 
+                           "of Cassandra.This operation will rewrite the sstables in the specified cf\n" + 
+                           "to match the currently installed version of Cassandra.\n" + 
+                           "The snapshot option will only upgrade the specified snapshot. Upgrading\n" + 
+                           "snapshots is required before attempting to restore a snapshot taken in a\n" + 
+                           "major version older than the major version Cassandra is currently running.\n" + 
+                           "This will replace the files in the given snapshot as well as break any\n" + 
+                           "hard links to live sstables.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           "    --debug         display stack traces\n" + 
+                           " -h,--help          display this help message\n" + 
+                           " -k,--keep-source   do not delete the source sstables\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "system_schema", "tables"))
+        {
+            assertTrue(tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "-k", "--keep-source").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertTrue("Arg: [" + arg + "]\n" + tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));
+                assertTrue("Arg: [" + arg + "]\n" + tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       Should use something like `Assertions.assertThat(tool.getCleanedStderr()).as("Arg: [%s]", arg).isEmpty();` will avoid the garbage and have similar error message.

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
##########
@@ -18,22 +18,33 @@
 
 package org.apache.cassandra.tools;
 
+import java.util.Arrays;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import org.apache.cassandra.OrderedJUnit4ClassRunner;
+import org.assertj.core.api.Assertions;
+import org.hamcrest.CoreMatchers;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 @RunWith(OrderedJUnit4ClassRunner.class)
 public class StandaloneUpgraderTest extends OfflineToolUtils
 {
     private ToolRunner.Runners runner = new ToolRunner.Runners();

Review comment:
       final

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testExtendedArg()
+    {
+        Arrays.asList("-e", "--extended").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("extended=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testQuickArg()
+    {
+        Arrays.asList("-q", "--quick").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("quick=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testRepairStatusArg()
+    {
+        Arrays.asList("-r", "--mutate_repair_status").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(),
+                                                       arg,
+                                                       "system_schema",
+                                                       "tables"))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("mutateRepairStatus=true"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+                tool.assertOnExitCode();

Review comment:
       this adds a compiler warning as this logic doesn't manage the returned resource, can you fix the compiler warnings?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
##########
@@ -43,12 +54,81 @@ public void testStandaloneUpgrader_NoArgs()
     }
 
     @Test
-    public void testStandaloneUpgrader_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneUpgrader", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "-h"))
+        {
+            String help = "usage: sstableupgrade [options] <keyspace> <cf> [snapshot]\n" + 
+                           "--\n" + 
+                           "Upgrade the sstables in the given cf (or snapshot) to the current version\n" + 
+                           "of Cassandra.This operation will rewrite the sstables in the specified cf\n" + 
+                           "to match the currently installed version of Cassandra.\n" + 
+                           "The snapshot option will only upgrade the specified snapshot. Upgrading\n" + 
+                           "snapshots is required before attempting to restore a snapshot taken in a\n" + 
+                           "major version older than the major version Cassandra is currently running.\n" + 
+                           "This will replace the files in the given snapshot as well as break any\n" + 
+                           "hard links to live sstables.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           "    --debug         display stack traces\n" + 
+                           " -h,--help          display this help message\n" + 
+                           " -k,--keep-source   do not delete the source sstables\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "system_schema", "tables"))
+        {
+            assertTrue(tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));

Review comment:
       can be simplified to 
   
   ```
   Assertions.asserThat(tool.getStdout()).isEqualTo("Found 0 sstables that need upgrading.\n");
   // or
   assertEquals(tool.getStdout(), "Found 0 sstables that need upgrading.\n", tool.getStdout());
   ```

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
##########
@@ -52,5 +67,101 @@ public void testStandaloneSplitter_NoArgs()
         assertServerNotLoaded();
     }
 
-    // Note: StandaloneSplitter modifies sstables
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "-h"))
+        {
+            String help = "usage: sstablessplit [options] <filename> [<filename>]*\n" + 
+                          "--\n" + 
+                          "Split the provided sstables files in sstables of maximum provided file\n" + 
+                          "size (see option --size).\n" + 
+                          "--\n" + 
+                          "Options are:\n" + 
+                          "    --debug         display stack traces\n" + 
+                          " -h,--help          display this help message\n" + 
+                          "    --no-snapshot   don't snapshot the sstables before splitting\n" + 
+                          " -s,--size <size>   maximum size in MB for the output sstables (default:\n" + 
+                          "                    50)\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "--debugwrong", "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testWrongFilename()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+            assertEquals(1,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "--no-snapshot").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), arg, "mockFile"))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                assertEquals(1,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testSizeArg()
+    {
+        Arrays.asList(Pair.of("-s", ""), Pair.of("-s", "w"), Pair.of("--size", ""), Pair.of("--size", "w"))
+              .forEach(arg -> {
+                  try
+                  {
+                      runner.invokeClassAsTool(StandaloneSplitter.class.getName(),

Review comment:
       this adds a compiler warning as this logic doesn't manage the returned resource, can you fix the compiler warnings?

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneUpgraderTest.java
##########
@@ -43,12 +54,81 @@ public void testStandaloneUpgrader_NoArgs()
     }
 
     @Test
-    public void testStandaloneUpgrader_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneUpgrader", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "-h"))
+        {
+            String help = "usage: sstableupgrade [options] <keyspace> <cf> [snapshot]\n" + 
+                           "--\n" + 
+                           "Upgrade the sstables in the given cf (or snapshot) to the current version\n" + 
+                           "of Cassandra.This operation will rewrite the sstables in the specified cf\n" + 
+                           "to match the currently installed version of Cassandra.\n" + 
+                           "The snapshot option will only upgrade the specified snapshot. Upgrading\n" + 
+                           "snapshots is required before attempting to restore a snapshot taken in a\n" + 
+                           "major version older than the major version Cassandra is currently running.\n" + 
+                           "This will replace the files in the given snapshot as well as break any\n" + 
+                           "hard links to live sstables.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           "    --debug         display stack traces\n" + 
+                           " -h,--help          display this help message\n" + 
+                           " -k,--keep-source   do not delete the source sstables\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneUpgrader.class.getName(), "system_schema", "tables"))
+        {
+            assertTrue(tool.getStdout(), tool.getStdout().equals("Found 0 sstables that need upgrading.\n"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       this produces a lot of garbage, it would be best to call once rather than twice for the message.  `Assertions.assertThat(tool.getCleanedStderr()).isEmpty()` would fix this

##########
File path: test/unit/org/apache/cassandra/tools/StandaloneSplitterTest.java
##########
@@ -52,5 +67,101 @@ public void testStandaloneSplitter_NoArgs()
         assertServerNotLoaded();
     }
 
-    // Note: StandaloneSplitter modifies sstables
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "-h"))
+        {
+            String help = "usage: sstablessplit [options] <filename> [<filename>]*\n" + 
+                          "--\n" + 
+                          "Split the provided sstables files in sstables of maximum provided file\n" + 
+                          "size (see option --size).\n" + 
+                          "--\n" + 
+                          "Options are:\n" + 
+                          "    --debug         display stack traces\n" + 
+                          " -h,--help          display this help message\n" + 
+                          "    --no-snapshot   don't snapshot the sstables before splitting\n" + 
+                          " -s,--size <size>   maximum size in MB for the output sstables (default:\n" + 
+                          "                    50)\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "--debugwrong", "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testWrongFilename()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), "mockFile"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+            assertEquals(1,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testFlagArgs()
+    {
+        Arrays.asList("--debug", "--no-snapshot").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeClassAsTool(StandaloneSplitter.class.getName(), arg, "mockFile"))
+            {
+                assertThat("Arg: [" + arg + "]", tool.getStdout(), CoreMatchers.containsStringIgnoringCase("Skipping inexisting file mockFile"));
+                assertThat("Arg: [" + arg + "]", tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("No valid sstables to split"));
+                assertEquals(1,tool.getExitCode());
+            }
+            assertCorrectEnvPostTest();
+        });
+    }
+
+    @Test
+    public void testSizeArg()
+    {
+        Arrays.asList(Pair.of("-s", ""), Pair.of("-s", "w"), Pair.of("--size", ""), Pair.of("--size", "w"))
+              .forEach(arg -> {
+                  try
+                  {
+                      runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
+                                               arg.getLeft(),
+                                               arg.getRight(),
+                                               "mockFile");
+                      fail("Shouldn't be able to parse wrong input as number");
+                  }
+                  catch(RuntimeException e)

Review comment:
       nit: can do something like the following
   
   ```
   Assertions.assertThatThrownBy(() -> {
                         try (ToolRunner ignored = runner.invokeClassAsTool(StandaloneSplitter.class.getName(),
                                                                            arg.getLeft(),
                                                                            arg.getRight(),
                                                                            "mockFile"))
                         {
   
                         }
                     }).isInstanceOf(RuntimeException.class)
                       .hasCauseInstanceOf(NumberFormatException.class);
   ```
   
   Does the same

##########
File path: test/unit/org/apache/cassandra/tools/SSTableExportTest.java
##########
@@ -43,12 +62,146 @@ public void testSSTableExport_NoArgs()
     }
 
     @Test
-    public void testSSTableExport_WithArgs() throws Exception
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableExport.class.getName()))
+        {
+            String help = "usage: sstabledump <sstable file path> <options>\n" + 
+                           "                   \n" + 
+                           "Dump contents of given SSTable to standard output in JSON format.\n" + 
+                           " -d         CQL row per line internal representation\n" + 
+                           " -e         enumerate partition keys only\n" + 
+                           " -k <arg>   Partition key\n" + 
+                           " -l         Output json lines, by partition\n" + 
+                           " -t         Print raw timestamps instead of iso8601 date strings\n" + 
+                           " -x <arg>   Excluded partition key\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableExport.class.getName(), "--debugwrong", findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport",findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertTrue(tool.getStdout(), parsed.get(0).get("partition") != null);
+            assertTrue(tool.getStdout(), parsed.get(0).get("rows") != null);
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testCQLRowArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-d"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.startsWith("[0]"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testPKOnlyArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-e"))
+        {
+            assertEquals(tool.getStdout(), "[ [ \"0\" ], [ \"1\" ], [ \"2\" ], [ \"3\" ], [ \"4\" ]\n]", tool.getStdout());
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testPKArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-k", "0"))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertEquals(tool.getStdout(), 1, parsed.size());
+            assertEquals(tool.getStdout(), "0", ((List) ((Map) parsed.get(0).get("partition")).get("key")).get(0));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testExcludePKArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-x", "0"))
+        {
+            List<Map<String, Object>> parsed = mapper.readValue(tool.getStdout(), jacksonListOfMapsType);
+            assertEquals(tool.getStdout(), 4, parsed.size());
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        }
+        assertPostTestEnv();
+    }
+
+    @Test
+    public void testTSFormatArg() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableExport", findOneSSTable("legacy_sstables", "legacy_ma_simple"), "-t"))
+        {
+            assertThat(tool.getStdout(),

Review comment:
       this should check the actual field has the correct value rather than just contains.

##########
File path: test/unit/org/apache/cassandra/tools/ToolsEnvsConfigsTest.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.util.ArrayList;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.apache.cassandra.OrderedJUnit4ClassRunner;
+import org.apache.cassandra.cql3.CQLTester;
+
+import static org.junit.Assert.assertTrue;
+
+@RunWith(OrderedJUnit4ClassRunner.class)
+public class ToolsEnvsConfigsTest
+{
+    //Some JDK can output env info on stdout/err. Check we can clean them
+    @Test
+    public void testJDKEnvInfoDefaultCleaners()
+    {
+        ToolRunner runner = new ToolRunner(CQLTester.buildNodetoolArgs(new ArrayList<String>()), true);

Review comment:
       nit: should use `Collections.emptyList`

##########
File path: test/unit/org/apache/cassandra/tools/SSTableRepairedAtSetterTest.java
##########
@@ -43,10 +56,73 @@ public void testSSTableRepairedAtSetter_NoArgs()
     }
 
     @Test
-    public void testSSTableRepairedAtSetter_WithArgs() throws Exception
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableRepairedAtSetter.class.getName(), "-h"))
+        {
+            String help = "This command should be run with Cassandra stopped, otherwise you will get very strange behavior\n" + 
+                          "Verify that Cassandra is not running and then execute the command like this:\n" + 
+                          "Usage: sstablerepairedset --really-set [--is-repaired | --is-unrepaired] [-f <sstable-list> | <sstables>]\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp() throws IOException
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableRepairedAtSetter.class.getName(),
+                                                       "--debugwrong",
+                                                       findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testIsrepairedArg() throws Exception
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.SSTableRepairedAtSetter", "--really-set", "--is-repaired", findOneSSTable("legacy_sstables", "legacy_ma_simple"))
-              .waitAndAssertOnCleanExit();
+        try (ToolRunner tool = runner.invokeClassAsTool(SSTableRepairedAtSetter.class.getName(),
+                                                       "--really-set",
+                                                       "--is-repaired",
+                                                       findOneSSTable("legacy_sstables", "legacy_ma_simple")))
+        {
+            tool.waitAndAssertOnCleanExit();
+        }
+        assertNoUnexpectedThreadsStarted(null, OPTIONAL_THREADS_WITH_SCHEMA);
+        assertSchemaNotLoaded();

Review comment:
       why not add all this logic in an `@After` method, so you don't need to repeat?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486707839



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());

Review comment:
       we capture the bytes, then toString it, then call `java.lang.String#replaceAll` for each pre-defined regex (which itself recompiles the Pattern).
   
   Might not be a lot of cost, but its a simple pattern to avoid so was mostly calling it out so we can avoid moving forward.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484196163



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))

Review comment:
       Atm `ToolRunner` is `AutoClosable` so it's good to close it in case in the future `close()` does sthg meaningful imo. Let's see how `ToolRunner` looks when we finish our discussions about it first, then we can revert/change this as it's a pain to change every time we go back/forth on it.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r485065274



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error

Review comment:
       > why don't we 'ignore' the offending assert alone?
   
   Just saw the change, so it looks like you comment out the exception; that is fine for now.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486070676



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();

Review comment:
       I'm sorry but I am not getting those. Neither I can manage to find in the settings which one would enable this to trigger of the many I can see...




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r481010405



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error

Review comment:
       Ok some explanation is needed here. On 15991 (this ticket) given the mammoth size of the main task (15583), I am only 'seeding' unit tests for all tools. These are focusing on testing the input args only. This has no other reason but to move forward at a pace I can handle and not bite off more than I can chew.
   
   On [CASSANDRA-15583](https://issues.apache.org/jira/browse/CASSANDRA-15583#)'s description you'll see I raised individual tickets to UT each tool. CASSANDRA-16021 is the one for the `AuditLogViewer`. So this fix you're referring to belongs in that ticket. Same for any lack of coverage or any other bugs we find in any other of the tools.
   
   Does it make sense? I'm just trying to break up the big task in manageable bits rather than fixing all problems on all the tools at once big-bang style.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r480365473



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+        Arrays.asList(Pair.of("-f", "-r"), Pair.of("--follow", "--roll_cycle")).stream().forEach(arg -> {

Review comment:
       nit: should do `Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach` so we cover all cases

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));

Review comment:
       should use `org.assertj.core.api.Assertions#assertThat(java.lang.String)`.  `assertTrue` doesn't have a useful error message so if someone does something like replace `usage:` with `Usage:` then the error is "there is an error"; by using `Assertions.assertThat(tool.getStdout()).contains("usage:")` you get a much more useful error message.

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {

Review comment:
       remove `.stream()`, forEach exists on list as well.

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()

Review comment:
       should merge this test with `testMaybeChangeDocs`

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));

Review comment:
       same comment as above

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));

Review comment:
       would be cleaner to copy the current stdout text and use in the test rather than md5.  If you use `Assertions.assertThat(tool.getStdout()).isEqualTo(expectedHelp)` you get a very useful error message showing what failed; with md5 we need to spend more time debugging.

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error

Review comment:
       This looks like a bug, do we not include the flag to disable this? 

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {

Review comment:
       remove `.stream()`

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, arg);
+            assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            assertTrue(tool.getCleanedStderr(),
+                       tool.getCleanedStderr().isEmpty() // j8 is fine
+                       || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+            assertEquals(0, tool.getExitCode());
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+        Arrays.asList(Pair.of("-f", "-r"), Pair.of("--follow", "--roll_cycle")).stream().forEach(arg -> {
+            ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                    path.toAbsolutePath().toString(),
+                                                                    arg.getLeft(),
+                                                                    arg.getRight(),
+                                                                    "TEST_SECONDLY"));
+            // Tool is running in the background 'following' so we have to kill it
+            assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       so you want to validate that we are still following?  Where do you actually kill it the process?

##########
File path: test/unit/org/apache/cassandra/tools/OfflineToolUtils.java
##########
@@ -110,7 +110,7 @@ public void assertNoUnexpectedThreadsStarted(String[] expectedThreadNames, Strin
                                     .filter(threadName -> optional.stream().anyMatch(pattern -> pattern.matcher(threadName).matches()))
                                     .collect(Collectors.toSet());
 
-        if (!current.isEmpty())
+        if (!remain.isEmpty())

Review comment:
       for me: confirmed that remaining should have been used




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r483132577



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+            try (ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                         path.toAbsolutePath().toString(),
+                                                                         arg.get(0),
+                                                                         arg.get(1),
+                                                                         "TEST_SECONDLY")))
+            {
+                // Tool is running in the background 'following' so we have to kill it
+                assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       I have issues with this given the current API, its why I am removing it in https://issues.apache.org/jira/browse/CASSANDRA-16082.
   
   The issue is that `ToolRunner` has methods for completed and non-completed process in the same API which make it hard to use if a process is not completed, for example the stdout/stderr logic you have below isn't thread safe (until CASSANDRA-16082 which only publishes on complete).  This test is a *useful* test, but I feel that `ToolRunner` should only be completed results, and a different interface should be for non-completed results.
   
   Also, if we want the latest logs when not completed, we will need to to redo how logging is done as it doesn't work atm and won't work in CASSANDRA-16082 as it blocks waiting for complete.
   
   I feel it would be best if `invokeToolNoWait` returns something like `ObservableTool` which lets you interact with a currently running process, and on complete get access to the completed result (atm `ToolRunner` but may be best to rename?).
   
   thoughts?

##########
File path: test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
##########
@@ -36,9 +36,11 @@
     @Test
     public void testBulkLoader_NoArgs() throws Exception
     {
-        ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader");
-        assertEquals(1, tool.getExitCode());
-        assertTrue(!tool.getStderr().isEmpty());
+        try (ToolRunner tool = runner.invokeClassAsTool("org.apache.cassandra.tools.BulkLoader"))
+        {
+            assertEquals(1, tool.getExitCode());
+            assertTrue(!tool.getCleanedStderr().isEmpty());

Review comment:
       I know you are only refactoring the existing test, but this test verifies an error exists, but not that it is the correct error.  If BulkLoader changes then this test will only fail if bulk loader does something like prints a help without failing.  I thin we should validate that error contains `Missing sstable directory argument` as this is the actual expected error.

##########
File path: test/unit/org/apache/cassandra/tools/ClearSnapshotTest.java
##########
@@ -54,32 +56,43 @@ public static void teardown() throws IOException
     @Test
     public void testClearSnapshot_NoArgs() throws IOException
     {
-        ToolRunner tool = runner.invokeNodetool("clearsnapshot");
-        assertEquals(2, tool.getExitCode());
-        assertTrue("Tool stderr: " +  tool.getStderr(), tool.getStderr().contains("Specify snapshot name or --all"));
+        try (ToolRunner tool = runner.invokeNodetool("clearsnapshot"))

Review comment:
       Can we remove the unneeded close?  Looks like all usage switched to calling close but `ToolRunner` really should not support any existing running process as the API doesn't properly work in this case.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r481002029



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+    }
+
+    @Test
+    public void testHelpArg()

Review comment:
       -1 here from me. I really want _docs_ to be a first class citizen concern regarding tooling. So rather than being 'an extra test line' I wanted to give it it's own method to highlight the importance of keeping help/docs updated. wdyt?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r485438825



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+            try (ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                         path.toAbsolutePath().toString(),
+                                                                         arg.get(0),
+                                                                         arg.get(1),
+                                                                         "TEST_SECONDLY")))
+            {
+                // Tool is running in the background 'following' so we have to kill it
+                assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       Thanks for the detailed explanation. I still prefer the `try{...}` approach but that is down to personal preference imo. I don't have a problem adopting your patch (and thx again for that). Also you're the first 'customer' of `ToolRunner` and it's useful customer feedback :-)
   
   Would you be ok continuing the review assuming CASSANDRA-16082 + your POC will go in? I'll rebase as soon as that merges. I think that is orthogonal-ish to being able to review the rest of the PR and it will safe me from rebasing a moving target until 16082 settles. Lately I just spend my days rebasing stuff :man_facepalming: 
   
   I'll leave this comment open as a reminder for me.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r483132577



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));
+        }
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, arg))
+            {
+                assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+                assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            try (ToolRunner tool = runner.invokeTool(toolPath, path.toAbsolutePath().toString(), arg))
+            {
+                assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+                assertTrue(tool.getCleanedStderr(),
+                           tool.getCleanedStderr().isEmpty() // j8 is fine
+                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+                assertEquals(0, tool.getExitCode());
+            }
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+            try (ToolRunner tool = runner.invokeToolNoWait(Arrays.asList(toolPath,
+                                                                         path.toAbsolutePath().toString(),
+                                                                         arg.get(0),
+                                                                         arg.get(1),
+                                                                         "TEST_SECONDLY")))
+            {
+                // Tool is running in the background 'following' so we have to kill it
+                assertFalse(tool.waitFor(3, TimeUnit.SECONDS));

Review comment:
       I have issues with this given the current API, its why I am removing it in https://issues.apache.org/jira/browse/CASSANDRA-16082.
   
   The issue is that `ToolRunner` has methods for completed and non-completed process in the same API which make it hard to use if a process is not completed, ~for example the stdout/stderr logic you have below isn't thread safe (until CASSANDRA-16082 which only publishes on complete)~ for example checking the logs before calling waitFor can lead to flaky tests.  This test is a *useful* test, but I feel that `ToolRunner` should only be completed results, and a different interface should be for non-completed results.
   
   ~Also, if we want the latest logs when not completed, we will need to to redo how logging is done as it doesn't work atm and won't work in CASSANDRA-16082 as it blocks waiting for complete.~ I was wrong, `ByteArrayOutputStream` syncs on every method call so reading/writing from multiple threads is safe.
   
   I feel it would be best if `invokeToolNoWait` returns something like `ObservableTool` which lets you interact with a currently running process, and on complete get access to the completed result (atm `ToolRunner` but may be best to rename?).
   
   thoughts?




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r494632308



##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();

Review comment:
       since ObservableTool is closable can you make this
   
   ```
   try (ObservableTool  t = invokeAsync(args))
   {
       return t.waitComplete();
   }
   ```

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();

Review comment:
       since ObservableTool is closable can you make this
   
   ```
   try (ObservableTool  t = invokeAsync(env, stdin, args))
   {
       return t.waitComplete();
   }
   ```

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);

Review comment:
       `newOut` and `newErr` are buffered so should call `.flush()` before calling `.toString()` on the backing byte array stream; in the common println case this wouldn't be an issue, but if a tool does print and fails in the middle then this might not show up in the output.

##########
File path: test/unit/org/apache/cassandra/tools/SSTableMetadataViewerTest.java
##########
@@ -43,9 +57,112 @@ public void testSSTableOfflineRelevel_NoArgs()
     }
 
     @Test
-    public void testSSTableOfflineRelevel_WithArgs()
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolResult tool = ToolRunner.invokeClass(SSTableMetadataViewer.class, "-h");
+        assertEquals("a30d3c489bcf56ddb2140184c5c62422", DigestUtils.md5Hex(tool.getCleanedStderr()));

Review comment:
       can you add the help string here rather than md5?

##########
File path: test/unit/org/apache/cassandra/tools/BulkLoaderTest.java
##########
@@ -75,20 +73,20 @@ public void testBulkLoader_WithArgs() throws Exception
     @Test
     public void testBulkLoader_WithArgs1() throws Exception
     {
-        try
-        {
-            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)
-        {
-            if (!(e.getCause() instanceof BulkLoadException))
-                throw e;
-            if (!(e.getCause().getCause() instanceof NoHostAvailableException))
-                throw e;
-        }
-        assertNoUnexpectedThreadsStarted(null, new String[]{"globalEventExecutor-1-1", "globalEventExecutor-1-2"});
+        ToolResult tool = ToolRunner.invokeClass(BulkLoader.class,
+                                                 "-d",
+                                                 "127.9.9.1",
+                                                 "--port",
+                                                 "9042",
+                                                 OfflineToolUtils.sstableDirName("legacy_sstables", "legacy_ma_simple"));
+
+        assertEquals(-1, tool.getExitCode());
+        if (!(tool.getException().getCause() instanceof BulkLoadException))

Review comment:
       nit: I think you can do the same with
   
   ```
   Assertions.assertThat(tool.getExitCode())
                     .hasCauseInstanceOf(BulkLoadException.class)
                     .hasRootCauseInstanceOf(NoHostAvailableException.class); // no host normally doesn't have a cause so this should match.
   ```

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);
+        }
+        catch (Exception e)
+        {
+            return new ToolResult(allArgs, -1, "", Throwables.getStackTraceAsString(e), e);

Review comment:
       shouldn't we use `out.toString(), err.toString()` as well?  if we NPE but log a lot, we would miss the log even though we have the exception in `ToolResult`

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);
+        }
+        catch (Exception e)
+        {
+            return new ToolResult(allArgs, -1, "", Throwables.getStackTraceAsString(e), e);
+        }
+        finally
+        {
+            System.setOut(originalSysOut);
+            System.setErr(originalSysErr);
+            System.setIn(originalSysIn);
         }
     }
 
-    private static final class StreamGobbler<T extends OutputStream> implements Runnable
+    public static Builder builder(List<String> args)
     {
-        private static final int BUFFER_SIZE = 8_192;
+        return new Builder(args);
+    }
 
-        private final InputStream input;
-        private final T out;
+    public static final class ToolResult
+    {
+        private final List<String> allArgs;
+        private final int exitCode;
+        private final String stdout;
+        private final String stderr;
+        private final Exception e;
 
-        private StreamGobbler(InputStream input, T out)
+        private ToolResult(List<String> allArgs, int exitCode, String stdout, String stderr, Exception e)
         {
-            this.input = input;
-            this.out = out;
+            this.allArgs = allArgs;
+            this.exitCode = exitCode;
+            this.stdout = stdout;
+            this.stderr = stderr;
+            this.e = e;
         }
 
-        public void run()
+        public int getExitCode()
         {
-            byte[] buffer = new byte[BUFFER_SIZE];
-            while (true)
-            {
-                try
-                {
-                    int read = input.read(buffer);
-                    if (read == -1)
-                    {
-                        return;
-                    }
-                    out.write(buffer, 0, read);
-                }
-                catch (IOException e)
-                {
-                    logger.error("Unexpected IO Error while reading stream", e);
-                    return;
-                }
-            }
+            return exitCode;
+        }
+
+        public String getStdout()
+        {
+            return stdout;
+        }
+
+        public String getStderr()
+        {
+            return stderr;
+        }
+        
+        public Exception getException()
+        {
+            return e;
         }
+        
+        /**
+         * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @return The ToolRunner instance
+         */
+        public void assertCleanStdErr()
+        {
+            assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(),
+                       getCleanedStderr().isEmpty());
+        }
+
+        public void assertOnExitCode()
+        {
+            assertExitCode(getExitCode());
+        }
+
+        private void assertExitCode(int code)
+        {
+            if (code != 0)
+                fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
+                                   argsToLogString(),
+                                   code,
+                                   getStderr(),
+                                   getStdout()));
+        }
+
+        public String argsToLogString()
+        {
+            return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output through the provided cleaners
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @param regExpCleaners
+         *            List of regExps to remove from stdErr
+         * @return The stdErr with all excludes removed
+         */
+        public String getCleanedStderr(List<String> regExpCleaners)
+        {
+            String sanitizedStderr = getStderr();
+            for (String regExp : regExpCleaners)
+                sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
+            return sanitizedStderr;
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output. Uses default list of excludes
+         * 
+         * {@link #getCleanedStderr(List)}
+         */
+        public String getCleanedStderr()
+        {
+            return getCleanedStderr(DEFAULT_CLEANERS);
+        }
+        
+        public void assertOnCleanExit()
+        {
+            assertOnExitCode();
+            assertCleanStdErr();
+        }
+    }
+
+    public interface ObservableTool extends AutoCloseable
+    {
+        String getPartialStdout();
+
+        String getPartialStderr();
+
+        boolean isDone();
+
+        ToolResult waitComplete();
+
+        @Override
+        void close();
     }
 
-    public static class Runners
+    private static final class FailedObservableTool implements ObservableTool
     {
-        public static ToolRunner invokeNodetool(String... args)
+        private final List<String> args;
+        private final IOException error;
+
+        private FailedObservableTool(IOException error, List<String> args)
+        {
+            this.args = args;
+            this.error = error;
+        }
+
+        @Override
+        public String getPartialStdout()
+        {
+            return "";
+        }
+
+        @Override
+        public String getPartialStderr()
+        {
+            return error.getMessage();
+        }
+
+        @Override
+        public boolean isDone()
         {
-            return invokeNodetool(Arrays.asList(args));
+            return true;
         }
 
-        public static ToolRunner invokeNodetool(List<String> args)
+        @Override
+        public ToolResult waitComplete()
         {
-            return invokeTool(buildNodetoolArgs(args), true);
+            return new ToolResult(args, -1, getPartialStdout(), getPartialStderr(), error);
         }
 
-        private static List<String> buildNodetoolArgs(List<String> args)
+        @Override
+        public void close()
         {
-            return CQLTester.buildNodetoolArgs(args);
+
         }
+    }
 
-        public static ToolRunner invokeClassAsTool(String... args)
+    private static final class ForkedObservableTool implements ObservableTool
+    {
+        private final CompletableFuture<Void> onComplete = new CompletableFuture<>();
+        private final ByteArrayOutputStream err = new ByteArrayOutputStream();

Review comment:
       would need to load in IntelliJ but I believe this would be an IntelliJ warning, so I think we should add `@SupressWarning("resources")`

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable

Review comment:
       is there a reason to make this generic?  I don't see us doing anything with it; I am ok if you want to keep it there.

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable
     {
-        return process != null && process.isAlive();
-    }
+        private static final int BUFFER_SIZE = 8_192;
 
-    public int waitFor()
-    {
-        try
+        private final InputStream input;
+        private final T out;
+
+        private StreamGobbler(InputStream input, T out)
         {
-            int rc = process.waitFor();
-            // must call first in order to make sure the stdin ioWatcher will exit
-            onComplete();
-            for (Thread t : ioWatchers)
-                t.join();
-            return rc;
+            this.input = input;
+            this.out = out;
         }
-        catch (InterruptedException e)
+
+        public void run()
         {
-            throw new RuntimeException(e);
+            byte[] buffer = new byte[BUFFER_SIZE];
+            while (true)
+            {
+                try
+                {
+                    int read = input.read(buffer);
+                    if (read == -1)
+                    {
+                        return;
+                    }
+                    out.write(buffer, 0, read);
+                }
+                catch (IOException e)
+                {
+                    logger.error("Unexpected IO Error while reading stream", e);
+                    return;
+                }
+            }
         }
     }
 
-    public ToolRunner waitAndAssertOnExitCode()
-    {
-        assertExitCode(waitFor());
-        return this;
-    }
-    
-    public ToolRunner waitAndAssertOnCleanExit()
+    /**
+     * Invokes Cqlsh. The first arg is the cql to execute
+     */
+    public static ToolResult invokeCqlsh(String... args)
     {
-        return waitAndAssertOnExitCode().assertCleanStdErr();
+        return invokeCqlsh(Arrays.asList(args));
     }
-    
+
     /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * @return The ToolRunner instance
+     * Invokes Cqlsh. The first arg is the cql to execute
      */
-    public ToolRunner assertCleanStdErr()
+    public static ToolResult invokeCqlsh(List<String> args)
     {
-        assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(), getCleanedStderr().isEmpty());
-        return this;
+        return invoke(CQLTester.buildCqlshArgs(args));
     }
 
-    public ToolRunner assertOnExitCode()
+    public static ToolResult invokeCassandraStress(String... args)
     {
-        assertExitCode(getExitCode());
-        return this;
+        return invokeCassandraStress(Arrays.asList(args));
     }
 
-    private void assertExitCode(int code)
+    public static ToolResult invokeCassandraStress(List<String> args)
     {
-        if (code != 0)
-            fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
-                               argsToLogString(),
-                               code,
-                               getStderr(),
-                               getStdout()));
+        return invoke(CQLTester.buildCassandraStressArgs(args));
     }
 
-    public String argsToLogString()
+    public static ToolResult invokeNodetool(String... args)
     {
-        return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        return invokeNodetool(Arrays.asList(args));
     }
 
-    public int getExitCode()
+    public static ToolResult invokeNodetool(List<String> args)
     {
-        return process.exitValue();
+        return invoke(CQLTester.buildNodetoolArgs(args));
     }
 
-    public String getStdout()
+    public static ToolResult invoke(List<String> args)
     {
-        return outBuffer.toString();
+        return invoke(args.toArray(new String[args.size()]));
     }
 
-    public String getStderr()
+    public static ToolResult invoke(String... args) 
     {
-        return errBuffer.toString();
+        return invokeAsync(args).waitComplete();
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
-     * 
-     * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual stdErr
-     * 
-     * @param regExpCleaners List of regExps to remove from stdErr
-     * @return The stdErr with all excludes removed
-     */
-    public String getCleanedStderr(List<String> regExpCleaners)
+    public static ObservableTool invokeAsync(String... args)
     {
-        String sanitizedStderr = getStderr();
-        for (String regExp: regExpCleaners)
-            sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
-        return sanitizedStderr;
+        return invokeAsync(Collections.emptyMap(), null, Arrays.asList(args));
     }
 
-    /**
-     * Checks if the stdErr is empty after removing any potential JVM env info output. Uses default list of excludes
-     * 
-     * {@link #getCleanedStderr(List)}
-     */
-    public String getCleanedStderr()
+    public static ToolResult invoke(Map<String, String> env, InputStream stdin, List<String> args)
     {
-        return getCleanedStderr(DEFAULT_CLEANERS);
+        return invokeAsync(env, stdin, args).waitComplete();
     }
 
-    public void forceKill()
+    public static ObservableTool invokeAsync(Map<String, String> env, InputStream stdin, List<String> args)
     {
+        ProcessBuilder pb = new ProcessBuilder(args);
+        if (env != null && !env.isEmpty())
+            pb.environment().putAll(env);
         try
         {
-            process.exitValue();
-            // process no longer alive - just ignore that fact
+            return new ForkedObservableTool(pb.start(), stdin, args);
         }
-        catch (IllegalThreadStateException e)
+        catch (IOException e)
         {
-            process.destroyForcibly();
+            return new FailedObservableTool(e, args);
         }
     }
 
-    @Override
-    public void close()
+    public static ToolResult invokeClass(String klass,  String... args)
+    {
+        return invokeClass(klass, null, args);
+    }
+
+    public static ToolResult invokeClass(Class<?> klass,  String... args)
     {
-        forceKill();
-        onComplete();
+        return invokeClass(klass.getName(), null, args);
     }
 
-    private void onComplete()
+    public static ToolResult invokeClass(String klass, InputStream stdin, String... args)
     {
-        if (stdin != null)
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(klass);
+        allArgs.addAll(Arrays.asList(args));
+        
+        PrintStream originalSysOut = System.out;
+        PrintStream originalSysErr = System.err;
+        InputStream originalSysIn = System.in;
+        originalSysOut.flush();
+        originalSysErr.flush();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+        System.setIn(stdin == null ? originalSysIn : stdin);
+
+        try (PrintStream newOut = new PrintStream(out);
+             PrintStream newErr = new PrintStream(err))
         {
-            try
-            {
-                stdin.close();
-            }
-            catch (IOException e)
-            {
-                logger.warn("Error closing stdin for {}", allArgs, e);
-            }
+            System.setOut(newOut);
+            System.setErr(newErr);
+            int rc = runClassAsTool(klass, args);
+            return new ToolResult(allArgs, rc, out.toString(), err.toString(), null);
+        }
+        catch (Exception e)
+        {
+            return new ToolResult(allArgs, -1, "", Throwables.getStackTraceAsString(e), e);
+        }
+        finally
+        {
+            System.setOut(originalSysOut);
+            System.setErr(originalSysErr);
+            System.setIn(originalSysIn);
         }
     }
 
-    private static final class StreamGobbler<T extends OutputStream> implements Runnable
+    public static Builder builder(List<String> args)
     {
-        private static final int BUFFER_SIZE = 8_192;
+        return new Builder(args);
+    }
 
-        private final InputStream input;
-        private final T out;
+    public static final class ToolResult
+    {
+        private final List<String> allArgs;
+        private final int exitCode;
+        private final String stdout;
+        private final String stderr;
+        private final Exception e;
 
-        private StreamGobbler(InputStream input, T out)
+        private ToolResult(List<String> allArgs, int exitCode, String stdout, String stderr, Exception e)
         {
-            this.input = input;
-            this.out = out;
+            this.allArgs = allArgs;
+            this.exitCode = exitCode;
+            this.stdout = stdout;
+            this.stderr = stderr;
+            this.e = e;
         }
 
-        public void run()
+        public int getExitCode()
         {
-            byte[] buffer = new byte[BUFFER_SIZE];
-            while (true)
-            {
-                try
-                {
-                    int read = input.read(buffer);
-                    if (read == -1)
-                    {
-                        return;
-                    }
-                    out.write(buffer, 0, read);
-                }
-                catch (IOException e)
-                {
-                    logger.error("Unexpected IO Error while reading stream", e);
-                    return;
-                }
-            }
+            return exitCode;
+        }
+
+        public String getStdout()
+        {
+            return stdout;
+        }
+
+        public String getStderr()
+        {
+            return stderr;
+        }
+        
+        public Exception getException()
+        {
+            return e;
         }
+        
+        /**
+         * Checks if the stdErr is empty after removing any potential JVM env info output and other noise
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @return The ToolRunner instance
+         */
+        public void assertCleanStdErr()
+        {
+            assertTrue("Failed because cleaned stdErr wasn't empty: " + getCleanedStderr(),
+                       getCleanedStderr().isEmpty());
+        }
+
+        public void assertOnExitCode()
+        {
+            assertExitCode(getExitCode());
+        }
+
+        private void assertExitCode(int code)
+        {
+            if (code != 0)
+                fail(String.format("%s%nexited with code %d%nstderr:%n%s%nstdout:%n%s",
+                                   argsToLogString(),
+                                   code,
+                                   getStderr(),
+                                   getStdout()));
+        }
+
+        public String argsToLogString()
+        {
+            return allArgs.stream().collect(Collectors.joining(",\n    ", "[", "]"));
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output through the provided cleaners
+         * 
+         * Some JVM configs may output env info on stdErr. We need to remove those to see what was the tool's actual
+         * stdErr
+         * 
+         * @param regExpCleaners
+         *            List of regExps to remove from stdErr
+         * @return The stdErr with all excludes removed
+         */
+        public String getCleanedStderr(List<String> regExpCleaners)
+        {
+            String sanitizedStderr = getStderr();
+            for (String regExp : regExpCleaners)
+                sanitizedStderr = sanitizedStderr.replaceAll(regExp, "");
+            return sanitizedStderr;
+        }
+
+        /**
+         * Returns stdErr after removing any potential JVM env info output. Uses default list of excludes
+         * 
+         * {@link #getCleanedStderr(List)}
+         */
+        public String getCleanedStderr()
+        {
+            return getCleanedStderr(DEFAULT_CLEANERS);
+        }
+        
+        public void assertOnCleanExit()
+        {
+            assertOnExitCode();
+            assertCleanStdErr();
+        }
+    }
+
+    public interface ObservableTool extends AutoCloseable
+    {
+        String getPartialStdout();
+
+        String getPartialStderr();
+
+        boolean isDone();
+
+        ToolResult waitComplete();
+
+        @Override
+        void close();
     }
 
-    public static class Runners
+    private static final class FailedObservableTool implements ObservableTool
     {
-        public static ToolRunner invokeNodetool(String... args)
+        private final List<String> args;
+        private final IOException error;
+
+        private FailedObservableTool(IOException error, List<String> args)
+        {
+            this.args = args;
+            this.error = error;
+        }
+
+        @Override
+        public String getPartialStdout()
+        {
+            return "";
+        }
+
+        @Override
+        public String getPartialStderr()
+        {
+            return error.getMessage();
+        }
+
+        @Override
+        public boolean isDone()
         {
-            return invokeNodetool(Arrays.asList(args));
+            return true;
         }
 
-        public static ToolRunner invokeNodetool(List<String> args)
+        @Override
+        public ToolResult waitComplete()
         {
-            return invokeTool(buildNodetoolArgs(args), true);
+            return new ToolResult(args, -1, getPartialStdout(), getPartialStderr(), error);
         }
 
-        private static List<String> buildNodetoolArgs(List<String> args)
+        @Override
+        public void close()
         {
-            return CQLTester.buildNodetoolArgs(args);
+
         }
+    }
 
-        public static ToolRunner invokeClassAsTool(String... args)
+    private static final class ForkedObservableTool implements ObservableTool
+    {
+        private final CompletableFuture<Void> onComplete = new CompletableFuture<>();

Review comment:
       I believe we talked about removing this in favor of just closing stdin directly in the other ticket; I am fine this way or directly.

##########
File path: test/unit/org/apache/cassandra/tools/ToolRunner.java
##########
@@ -273,228 +109,466 @@ public void checkPermission(Permission perm, Object context)
         }
     }
 
-    public boolean isRunning()
+    private static final class StreamGobbler<T extends OutputStream> implements Runnable

Review comment:
       did I do this in my POC?  If so I think I did it because I had some callback logic I eventually removed as it turned out to be less useful.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484196384



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +73,75 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        try (ToolRunner tool = runner.invokeTool(toolPath))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+            assertEquals(1, tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeTool(toolPath, "-h"))
+        {
+            assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));

Review comment:
       ok ok. I see you both mentioned it so apparently my 'great' idea to md5 this was not that 'great'. I will change it... lol




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r494642286



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +74,87 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolResult tool = ToolRunner.invoke(toolPath);
+        assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+        assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolResult tool = ToolRunner.invoke(toolPath, "-h");
+        String help = "usage: auditlogviewer <path1> [<path2>...<pathN>] [options]\n" + 
+                       "--\n" + 
+                       "View the audit log contents in human readable format\n" + 
+                       "--\n" + 
+                       "Options are:\n" + 
+                       " -f,--follow             Upon reacahing the end of the log continue\n" + 
+                       "                         indefinitely waiting for more records\n" + 
+                       " -h,--help               display this help message\n" + 
+                       " -i,--ignore             Silently ignore unsupported records\n" + 
+                       " -r,--roll_cycle <arg>   How often to roll the log file was rolled. May be\n" + 
+                       "                         necessary for Chronicle to correctly parse file names. (MINUTELY, HOURLY,\n" + 
+                       "                         DAILY). Default HOURLY.\n";
+        Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            ToolResult tool = ToolRunner.invoke(toolPath, arg);
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            ToolResult tool = ToolRunner.invoke(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            // @IgnoreAssert see CASSANDRA-16021
+//                assertTrue(tool.getCleanedStderr(),
+//                           tool.getCleanedStderr().isEmpty() // j8 is fine
+//                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+            tool.assertOnExitCode();
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+                ObservableTool tool = ToolRunner.invokeAsync(toolPath,

Review comment:
       since this is closable can you wrap in a try-with-resource block?

##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +74,87 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolResult tool = ToolRunner.invoke(toolPath);
+        assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+        assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolResult tool = ToolRunner.invoke(toolPath, "-h");
+        String help = "usage: auditlogviewer <path1> [<path2>...<pathN>] [options]\n" + 
+                       "--\n" + 
+                       "View the audit log contents in human readable format\n" + 
+                       "--\n" + 
+                       "Options are:\n" + 
+                       " -f,--follow             Upon reacahing the end of the log continue\n" + 
+                       "                         indefinitely waiting for more records\n" + 
+                       " -h,--help               display this help message\n" + 
+                       " -i,--ignore             Silently ignore unsupported records\n" + 
+                       " -r,--roll_cycle <arg>   How often to roll the log file was rolled. May be\n" + 
+                       "                         necessary for Chronicle to correctly parse file names. (MINUTELY, HOURLY,\n" + 
+                       "                         DAILY). Default HOURLY.\n";
+        Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+    }
+
+    @Test
+    public void testHelpArg()
+    {
+        Arrays.asList("-h", "--help").forEach(arg -> {
+            ToolResult tool = ToolRunner.invoke(toolPath, arg);
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertTrue(tool.getCleanedStderr(),tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();
+        });
+    }
+
+    @Test
+    public void testIgnoreArg()
+    {
+        Arrays.asList("-i", "--ignore").forEach(arg -> {
+            ToolResult tool = ToolRunner.invoke(toolPath, path.toAbsolutePath().toString(), arg);
+            assertTrue(tool.getStdout(), tool.getStdout().isEmpty());
+            // @IgnoreAssert see CASSANDRA-16021
+//                assertTrue(tool.getCleanedStderr(),
+//                           tool.getCleanedStderr().isEmpty() // j8 is fine
+//                           || tool.getCleanedStderr().startsWith("WARNING: An illegal reflective access operation has occurred")); //j11 throws an error
+            tool.assertOnExitCode();
+        });
+    }
+
+    @Test
+    public void testFollowNRollArgs()
+    {
+            Lists.cartesianProduct(Arrays.asList("-f", "--follow"), Arrays.asList("-r", "--roll_cycle")).forEach(arg -> {
+                ObservableTool tool = ToolRunner.invokeAsync(toolPath,

Review comment:
       the close on line 154 might not happen if the assert true throws.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on pull request #704:
URL: https://github.com/apache/cassandra/pull/704#issuecomment-691829599


   CASSANDRA-16082 got the +1 from John Meredith so that should merge soon iiuc and I'd be able to rebase + add your POC here soon. #collaborating


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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] bereng commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
bereng commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r484200700



##########
File path: test/unit/org/apache/cassandra/tools/AuditLogViewerTest.java
##########
@@ -61,6 +71,65 @@ public void tearDown() throws IOException
         }
     }
 
+    @Test
+    public void testNoArgs()
+    {
+        ToolRunner tool = runner.invokeTool(toolPath);
+        assertTrue(tool.getStdout(), tool.getStdout().contains("usage:"));
+        assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().contains("Audit log files directory path is a required argument."));
+        assertEquals(1, tool.getExitCode());
+    }
+
+    @Test
+    public void testMaybeChangeDocs()
+    {
+        // If you added, modified options or help, please update docs if necessary
+        ToolRunner tool = runner.invokeTool(toolPath, "-h");
+        assertEquals("34aea94756d4f6760b836dfbfa7bbf06", DigestUtils.md5Hex(tool.getStdout()));

Review comment:
       As per a later comment it seems embedding the help message itself is preferred so I will do that.




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org


[GitHub] [cassandra] dcapwell commented on a change in pull request #704: Cassandra 15991

Posted by GitBox <gi...@apache.org>.
dcapwell commented on a change in pull request #704:
URL: https://github.com/apache/cassandra/pull/704#discussion_r486707148



##########
File path: test/unit/org/apache/cassandra/tools/StandaloneVerifierTest.java
##########
@@ -43,12 +54,143 @@ public void testStandaloneVerifier_NoArgs()
     }
 
     @Test
-    public void testStandaloneVerifier_WithArgs()
+    public void testMaybeChangeDocs()
     {
-        runner.invokeClassAsTool("org.apache.cassandra.tools.StandaloneVerifier", "--debug", "system_schema", "tables")
-              .waitAndAssertOnCleanExit();
-        assertNoUnexpectedThreadsStarted(EXPECTED_THREADS_WITH_SCHEMA, OPTIONAL_THREADS_WITH_SCHEMA);
-        assertSchemaLoaded();
-        assertServerNotLoaded();
+        // If you added, modified options or help, please update docs if necessary
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "-h"))
+        {
+            String help = "usage: sstableverify [options] <keyspace> <column_family>\n" + 
+                           "--\n" + 
+                           "Verify the sstable for the provided table.\n" + 
+                           "--\n" + 
+                           "Options are:\n" + 
+                           " -c,--check_version          make sure sstables are the latest version\n" + 
+                           "    --debug                  display stack traces\n" + 
+                           " -e,--extended               extended verification\n" + 
+                           " -h,--help                   display this help message\n" + 
+                           " -q,--quick                  do a quick check, don't read all data\n" + 
+                           " -r,--mutate_repair_status   don't mutate repair status\n" + 
+                           " -t,--token_range <range>    long token range of the format left,right.\n" + 
+                           "                             This may be provided multiple times to define multiple different ranges\n" + 
+                           " -v,--verbose                verbose output\n";
+            Assertions.assertThat(tool.getStdout()).isEqualTo(help);
+        }
+    }
+
+    @Test
+    public void testWrongArgFailsAndPrintsHelp()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debugwrong", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("usage:"));
+            assertThat(tool.getCleanedStderr(), CoreMatchers.containsStringIgnoringCase("Unrecognized option"));
+            assertEquals(1,tool.getExitCode());
+        }
+    }
+
+    @Test
+    public void testDefaultCall()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("using the following options"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            assertEquals(0,tool.getExitCode());
+        }
+        assertCorrectEnvPostTest();
+    }
+
+    @Test
+    public void testDebugArg()
+    {
+        try (ToolRunner tool = runner.invokeClassAsTool(StandaloneVerifier.class.getName(), "--debug", "system_schema", "tables"))
+        {
+            assertThat(tool.getStdout(), CoreMatchers.containsStringIgnoringCase("debug=true"));
+            assertTrue(tool.getCleanedStderr(), tool.getCleanedStderr().isEmpty());
+            tool.assertOnExitCode();

Review comment:
       really?  weird.  If I delete the project in IntelliJ (or clone different repo) and run `ant realclean && ant && ant generate-idea-files` they show up for me.
   
   The project has a lot so can't block in the build, so right now its enforced on review =(




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

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



---------------------------------------------------------------------
To unsubscribe, e-mail: pr-unsubscribe@cassandra.apache.org
For additional commands, e-mail: pr-help@cassandra.apache.org