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

ddanielr pushed a commit to branch 2.1
in repository https://gitbox.apache.org/repos/asf/accumulo.git


The following commit(s) were added to refs/heads/2.1 by this push:
     new b874c0ed66 Adds --dry-run option to Merge (#6437)
b874c0ed66 is described below

commit b874c0ed66debf54378ab2971e46f77909f2f7ea
Author: Daniel Roberts <[email protected]>
AuthorDate: Mon Jun 22 15:14:18 2026 -0400

    Adds --dry-run option to Merge (#6437)
    
    * Adds --dry-run option to Merge
    
    Adds the --dry-run option to both the Shell's MergeCommand and the Merge
    utility code.
    
    Fixes an issue where MergeExceptions were being wrapped needlessly.
    
    Adds Mock tests for MergeCommand.
    Adds MergeIT tests.
    
    Adds explicit failure message for attempting to merge the accumulo.root
    table instead of just silently doing nothing.
---
 .../java/org/apache/accumulo/core/util/Merge.java  |  58 +++++++----
 .../org/apache/accumulo/core/util/MergeTest.java   |  20 ++--
 .../accumulo/shell/commands/MergeCommand.java      |  68 +++++++++----
 .../accumulo/shell/commands/MergeCommandTest.java  | 113 +++++++++++++++++++++
 .../apache/accumulo/test/functional/MergeIT.java   |  75 +++++++++++++-
 5 files changed, 285 insertions(+), 49 deletions(-)

diff --git a/core/src/main/java/org/apache/accumulo/core/util/Merge.java 
b/core/src/main/java/org/apache/accumulo/core/util/Merge.java
index 41f5a67943..9bd5dcc673 100644
--- a/core/src/main/java/org/apache/accumulo/core/util/Merge.java
+++ b/core/src/main/java/org/apache/accumulo/core/util/Merge.java
@@ -37,6 +37,7 @@ import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.data.TableId;
 import org.apache.accumulo.core.dataImpl.KeyExtent;
 import org.apache.accumulo.core.metadata.MetadataTable;
+import org.apache.accumulo.core.metadata.RootTable;
 import org.apache.accumulo.core.metadata.schema.DataFileValue;
 import org.apache.accumulo.core.metadata.schema.TabletsMetadata;
 import org.apache.accumulo.core.trace.TraceUtil;
@@ -63,7 +64,7 @@ public class Merge {
   private static final Logger log = LoggerFactory.getLogger(Merge.class);
 
   protected void message(String format, Object... args) {
-    log.info(String.format(format, args));
+    log.info("{}", String.format(format, args));
   }
 
   public static class MemoryConverter implements IStringConverter<Long> {
@@ -95,6 +96,9 @@ public class Merge {
     Text begin = null;
     @Parameter(names = {"-e", "--end"}, description = "end tablet", converter 
= TextConverter.class)
     Text end = null;
+    @Parameter(names = {"--dry-run"},
+        description = "Will only list which tablets ranges it plans on 
merging. No merge will be performed")
+    boolean dryRun = false;
   }
 
   public void start(String[] args) throws MergeException {
@@ -112,11 +116,18 @@ public class Merge {
         if (opts.goalSize == null || opts.goalSize < 1) {
           AccumuloConfiguration tableConfig =
               new 
ConfigurationCopy(client.tableOperations().getConfiguration(opts.tableName));
-          opts.goalSize = 
tableConfig.getAsBytes(Property.TABLE_SPLIT_THRESHOLD);
+          long newGoalSize = 
tableConfig.getAsBytes(Property.TABLE_SPLIT_THRESHOLD);
+          message("Invalid goal size: " + opts.goalSize + " Using the "
+              + Property.TABLE_SPLIT_THRESHOLD.getKey() + " value of : " + 
newGoalSize);
+          opts.goalSize = newGoalSize;
         }
 
         message("Merging tablets in table %s to %d bytes", opts.tableName, 
opts.goalSize);
-        mergomatic(client, opts.tableName, opts.begin, opts.end, 
opts.goalSize, opts.force);
+        mergomatic(client, opts.tableName, opts.begin, opts.end, 
opts.goalSize, opts.force,
+            opts.dryRun);
+      } catch (MergeException e) {
+        TraceUtil.setException(span, e, true);
+        throw e;
       } catch (Exception ex) {
         TraceUtil.setException(span, ex, true);
         throw new MergeException(ex);
@@ -142,10 +153,10 @@ public class Merge {
   }
 
   public void mergomatic(AccumuloClient client, String table, Text start, Text 
end, long goalSize,
-      boolean force) throws MergeException {
+      boolean force, boolean dryRun) throws MergeException {
     try {
-      if (table.equals(MetadataTable.NAME)) {
-        throw new IllegalArgumentException("cannot merge tablets on the 
metadata table");
+      if (table.equals(MetadataTable.NAME) || table.equals(RootTable.NAME)) {
+        throw new IllegalArgumentException("cannot merge tablets on the " + 
table + " table");
       }
       List<Size> sizes = new ArrayList<>();
       long totalSize = 0;
@@ -156,11 +167,11 @@ public class Merge {
         totalSize += next.size;
         sizes.add(next);
         if (totalSize > goalSize) {
-          totalSize = mergeMany(client, table, sizes, goalSize, force, false);
+          totalSize = mergeMany(client, table, sizes, goalSize, force, false, 
dryRun);
         }
       }
       if (sizes.size() > 1) {
-        mergeMany(client, table, sizes, goalSize, force, true);
+        mergeMany(client, table, sizes, goalSize, force, true, dryRun);
       }
     } catch (Exception ex) {
       throw new MergeException(ex);
@@ -168,7 +179,7 @@ public class Merge {
   }
 
   protected long mergeMany(AccumuloClient client, String table, List<Size> 
sizes, long goalSize,
-      boolean force, boolean last) throws MergeException {
+      boolean force, boolean last, boolean dryRun) throws MergeException {
     // skip the big tablets, which will be the typical case
     while (!sizes.isEmpty()) {
       if (sizes.get(0).size < goalSize) {
@@ -192,13 +203,13 @@ public class Merge {
     }
 
     if (numToMerge > 1) {
-      mergeSome(client, table, sizes, numToMerge);
+      mergeSome(client, table, sizes, numToMerge, dryRun);
     } else {
       if (numToMerge == 1 && sizes.size() > 1) {
         // here we have the case of a merge candidate that is surrounded by 
candidates that would
         // split
         if (force) {
-          mergeSome(client, table, sizes, 2);
+          mergeSome(client, table, sizes, 2, dryRun);
         } else {
           sizes.remove(0);
         }
@@ -206,7 +217,7 @@ public class Merge {
     }
     if (numToMerge == 0 && sizes.size() > 1 && last) {
       // That's the last tablet, and we have a bunch to merge
-      mergeSome(client, table, sizes, sizes.size());
+      mergeSome(client, table, sizes, sizes.size(), dryRun);
     }
     long result = 0;
     for (Size s : sizes) {
@@ -215,16 +226,16 @@ public class Merge {
     return result;
   }
 
-  protected void mergeSome(AccumuloClient client, String table, List<Size> 
sizes, int numToMerge)
-      throws MergeException {
-    merge(client, table, sizes, numToMerge);
+  protected void mergeSome(AccumuloClient client, String table, List<Size> 
sizes, int numToMerge,
+      boolean dryRun) throws MergeException {
+    merge(client, table, sizes, numToMerge, dryRun);
     for (int i = 0; i < numToMerge; i++) {
       sizes.remove(0);
     }
   }
 
-  protected void merge(AccumuloClient client, String table, List<Size> sizes, 
int numToMerge)
-      throws MergeException {
+  protected void merge(AccumuloClient client, String table, List<Size> sizes, 
int numToMerge,
+      boolean dryRun) throws MergeException {
     try {
       Text start = sizes.get(0).extent.prevEndRow();
       Text end = sizes.get(numToMerge - 1).extent.endRow();
@@ -233,13 +244,22 @@ public class Merge {
               : Key.toPrintableString(start.getBytes(), 0, start.getLength(), 
start.getLength()),
           end == null ? "+inf"
               : Key.toPrintableString(end.getBytes(), 0, end.getLength(), 
end.getLength()));
+      if (dryRun) {
+        message("dry-run would have started a Fate Merge for table %s tablet 
range (%s to %s]",
+            table,
+            start == null ? "-inf"
+                : Key.toPrintableString(start.getBytes(), 0, 
start.getLength(), start.getLength()),
+            end == null ? "+inf"
+                : Key.toPrintableString(end.getBytes(), 0, end.getLength(), 
end.getLength()));
+        return;
+      }
       client.tableOperations().merge(table, start, end);
     } catch (Exception ex) {
       throw new MergeException(ex);
     }
   }
 
-  protected Iterator<Size> getSizeIterator(AccumuloClient client, String 
tablename, Text start,
+  protected Iterator<Size> getSizeIterator(AccumuloClient client, String 
tableName, Text start,
       Text end) throws MergeException {
     // open up metadata, walk through the tablets.
 
@@ -247,7 +267,7 @@ public class Merge {
     TabletsMetadata tablets;
     try {
       ClientContext context = (ClientContext) client;
-      tableId = context.getTableId(tablename);
+      tableId = context.getTableId(tableName);
       tablets = TabletsMetadata.builder(context).scanMetadataTable()
           .overRange(new KeyExtent(tableId, end, 
start).toMetaRange()).fetch(FILES, PREV_ROW)
           .build();
diff --git a/core/src/test/java/org/apache/accumulo/core/util/MergeTest.java 
b/core/src/test/java/org/apache/accumulo/core/util/MergeTest.java
index 6d253fe0ae..ad1a699370 100644
--- a/core/src/test/java/org/apache/accumulo/core/util/MergeTest.java
+++ b/core/src/test/java/org/apache/accumulo/core/util/MergeTest.java
@@ -57,7 +57,7 @@ public class MergeTest {
     protected void message(String format, Object... args) {}
 
     @Override
-    protected Iterator<Size> getSizeIterator(AccumuloClient client, String 
tablename,
+    protected Iterator<Size> getSizeIterator(AccumuloClient client, String 
tableName,
         final Text start, final Text end) throws MergeException {
       final Iterator<Size> impl = tablets.iterator();
       return new Iterator<>() {
@@ -103,8 +103,8 @@ public class MergeTest {
     }
 
     @Override
-    protected void merge(AccumuloClient client, String table, List<Size> 
sizes, int numToMerge)
-        throws MergeException {
+    protected void merge(AccumuloClient client, String table, List<Size> 
sizes, int numToMerge,
+        boolean dryRun) throws MergeException {
       List<Size> merge = new ArrayList<>();
       for (int i = 0; i < numToMerge; i++) {
         merge.add(sizes.get(i));
@@ -127,20 +127,20 @@ public class MergeTest {
     // Merge everything to the last tablet
     int i;
     MergeTester test = new MergeTester(10, 20, 30);
-    test.mergomatic(null, "table", null, null, 1000, false);
+    test.mergomatic(null, "table", null, null, 1000, false, false);
     assertEquals(1, test.merges.size());
     assertArrayEquals(new int[] {10, 20, 30}, sizes(test.merges.get(i = 0)));
 
     // Merge ranges around tablets that are big enough
     test = new MergeTester(1, 2, 100, 1000, 17, 1000, 4, 5, 6, 900);
-    test.mergomatic(null, "table", null, null, 1000, false);
+    test.mergomatic(null, "table", null, null, 1000, false, false);
     assertEquals(2, test.merges.size());
     assertArrayEquals(new int[] {1, 2, 100}, sizes(test.merges.get(i = 0)));
     assertArrayEquals(new int[] {4, 5, 6, 900}, sizes(test.merges.get(++i)));
 
     // Test the force option
     test = new MergeTester(1, 2, 100, 1000, 17, 1000, 4, 5, 6, 900);
-    test.mergomatic(null, "table", null, null, 1000, true);
+    test.mergomatic(null, "table", null, null, 1000, true, false);
     assertEquals(3, test.merges.size());
     assertArrayEquals(new int[] {1, 2, 100}, sizes(test.merges.get(i = 0)));
     assertArrayEquals(new int[] {17, 1000}, sizes(test.merges.get(++i)));
@@ -148,25 +148,25 @@ public class MergeTest {
 
     // Limit the low-end of the merges
     test = new MergeTester(1, 2, 1000, 17, 1000, 4, 5, 6, 900);
-    test.mergomatic(null, "table", new Text("00004"), null, 1000, false);
+    test.mergomatic(null, "table", new Text("00004"), null, 1000, false, 
false);
     assertEquals(1, test.merges.size());
     assertArrayEquals(new int[] {4, 5, 6, 900}, sizes(test.merges.get(i = 0)));
 
     // Limit the upper end of the merges
     test = new MergeTester(1, 2, 1000, 17, 1000, 4, 5, 6, 900);
-    test.mergomatic(null, "table", null, new Text("00004"), 1000, false);
+    test.mergomatic(null, "table", null, new Text("00004"), 1000, false, 
false);
     assertEquals(1, test.merges.size());
     assertArrayEquals(new int[] {1, 2}, sizes(test.merges.get(i = 0)));
 
     // Limit both ends
     test = new MergeTester(1, 2, 1000, 17, 1000, 4, 5, 6, 900);
-    test.mergomatic(null, "table", new Text("00002"), new Text("00004"), 1000, 
true);
+    test.mergomatic(null, "table", new Text("00002"), new Text("00004"), 1000, 
true, false);
     assertEquals(1, test.merges.size());
     assertArrayEquals(new int[] {17, 1000}, sizes(test.merges.get(i = 0)));
 
     // Clump up tablets into larger values
     test = new MergeTester(100, 250, 500, 600, 100, 200, 500, 200);
-    test.mergomatic(null, "table", null, null, 1000, false);
+    test.mergomatic(null, "table", null, null, 1000, false, false);
     assertEquals(3, test.merges.size());
     assertArrayEquals(new int[] {100, 250, 500}, sizes(test.merges.get(i = 
0)));
     assertArrayEquals(new int[] {600, 100, 200}, sizes(test.merges.get(++i)));
diff --git 
a/shell/src/main/java/org/apache/accumulo/shell/commands/MergeCommand.java 
b/shell/src/main/java/org/apache/accumulo/shell/commands/MergeCommand.java
index d6a74fce23..139402c0d0 100644
--- a/shell/src/main/java/org/apache/accumulo/shell/commands/MergeCommand.java
+++ b/shell/src/main/java/org/apache/accumulo/shell/commands/MergeCommand.java
@@ -18,7 +18,11 @@
  */
 package org.apache.accumulo.shell.commands;
 
+import org.apache.accumulo.core.client.AccumuloException;
+import org.apache.accumulo.core.client.AccumuloSecurityException;
+import org.apache.accumulo.core.client.TableNotFoundException;
 import org.apache.accumulo.core.conf.ConfigurationTypeHelper;
+import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.util.Merge;
 import org.apache.accumulo.shell.Shell;
 import org.apache.accumulo.shell.Shell.Command;
@@ -28,7 +32,7 @@ import org.apache.commons.cli.Options;
 import org.apache.hadoop.io.Text;
 
 public class MergeCommand extends Command {
-  private Option verboseOpt, forceOpt, sizeOpt, allOpt;
+  private Option verboseOpt, forceOpt, sizeOpt, allOpt, dryRunOpt;
 
   @Override
   public int execute(final String fullCommand, final CommandLine cl, final 
Shell shellState)
@@ -36,6 +40,7 @@ public class MergeCommand extends Command {
     boolean verbose = shellState.isVerbose();
     boolean force = false;
     boolean all = false;
+    boolean dryRun = false;
     long size = -1;
     final String tableName = OptUtil.getTableOpt(cl, shellState);
     final Text startRow = OptUtil.getStartRow(cl);
@@ -52,6 +57,9 @@ public class MergeCommand extends Command {
     if (cl.hasOption(sizeOpt.getOpt())) {
       size = 
ConfigurationTypeHelper.getFixedMemoryAsBytes(cl.getOptionValue(sizeOpt.getOpt()));
     }
+    if (cl.hasOption(dryRunOpt)) {
+      dryRun = true;
+    }
     if (startRow == null && endRow == null && size < 0 && !all) {
       if (!shellState
           .confirm(" Warning!!! Are you REALLY sure you want to merge the 
entire table { "
@@ -60,21 +68,7 @@ public class MergeCommand extends Command {
         return 0;
       }
     }
-    if (size < 0) {
-      shellState.getAccumuloClient().tableOperations().merge(tableName, 
startRow, endRow);
-    } else {
-      final boolean finalVerbose = verbose;
-      final Merge merge = new Merge() {
-        @Override
-        protected void message(String fmt, Object... args) {
-          if (finalVerbose) {
-            shellState.getWriter().println(String.format(fmt, args));
-          }
-        }
-      };
-      merge.mergomatic(shellState.getAccumuloClient(), tableName, startRow, 
endRow, size, force);
-    }
-    return 0;
+    return executeMerge(shellState, tableName, startRow, endRow, size, 
verbose, force, dryRun);
   }
 
   @Override
@@ -96,10 +90,14 @@ public class MergeCommand extends Command {
     forceOpt = new Option("f", "force", false,
         "merge small tablets to large tablets, even if it goes over the given 
size");
     // Using the constructor does not allow for empty option
-    Option.Builder builder = Option.builder().longOpt("all").hasArg(false)
+    Option.Builder allBuilder = Option.builder().longOpt("all").hasArg(false)
         .desc("allow an entire table to be merged into one tablet without 
prompting"
             + " the user for confirmation");
-    allOpt = builder.build();
+    allOpt = allBuilder.build();
+    Option.Builder dryRunBuilder = 
Option.builder().longOpt("dry-run").hasArg(false)
+        .desc("print the ranges it will merge, but do not perform any merge 
operations");
+    dryRunOpt = dryRunBuilder.build();
+
     o.addOption(OptUtil.startRowOpt());
     o.addOption(OptUtil.endRowOpt());
     o.addOption(OptUtil.tableOpt("table to be merged"));
@@ -107,7 +105,41 @@ public class MergeCommand extends Command {
     o.addOption(sizeOpt);
     o.addOption(forceOpt);
     o.addOption(allOpt);
+    o.addOption(dryRunOpt);
     return o;
   }
 
+  // This method is stubbed out to allow for mock testing
+  int executeMerge(Shell shellState, String tableName, Text startRow, Text 
endRow, long size,
+      boolean verbose, boolean force, boolean dryRun) throws AccumuloException,
+      TableNotFoundException, AccumuloSecurityException, Merge.MergeException {
+    if (size < 0) {
+      if (dryRun) {
+        shellState.getWriter()
+            .println(String.format(
+                "dry-run would have started a Fate Merge for table %s tablet 
range (%s to %s]",
+                tableName,
+                startRow == null ? "-inf"
+                    : Key.toPrintableString(startRow.getBytes(), 0, 
startRow.getLength(),
+                        startRow.getLength()),
+                endRow == null ? "+inf" : 
Key.toPrintableString(endRow.getBytes(), 0,
+                    endRow.getLength(), endRow.getLength())));
+        return 0;
+      }
+      shellState.getAccumuloClient().tableOperations().merge(tableName, 
startRow, endRow);
+    } else {
+      final boolean finalVerbose = verbose;
+      final Merge merge = new Merge() {
+        @Override
+        protected void message(String fmt, Object... args) {
+          if (finalVerbose) {
+            shellState.getWriter().println(String.format(fmt, args));
+          }
+        }
+      };
+      merge.mergomatic(shellState.getAccumuloClient(), tableName, startRow, 
endRow, size, force,
+          dryRun);
+    }
+    return 0;
+  }
 }
diff --git 
a/shell/src/test/java/org/apache/accumulo/shell/commands/MergeCommandTest.java 
b/shell/src/test/java/org/apache/accumulo/shell/commands/MergeCommandTest.java
index 42c85f30a3..4762553299 100644
--- 
a/shell/src/test/java/org/apache/accumulo/shell/commands/MergeCommandTest.java
+++ 
b/shell/src/test/java/org/apache/accumulo/shell/commands/MergeCommandTest.java
@@ -18,12 +18,61 @@
  */
 package org.apache.accumulo.shell.commands;
 
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.newCapture;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.io.PrintWriter;
+import java.util.Optional;
+
+import org.apache.accumulo.core.client.AccumuloClient;
+import org.apache.accumulo.core.client.AccumuloException;
+import org.apache.accumulo.core.client.AccumuloSecurityException;
+import org.apache.accumulo.core.client.TableNotFoundException;
+import org.apache.accumulo.core.client.admin.InstanceOperations;
+import org.apache.accumulo.core.client.admin.TableOperations;
+import org.apache.accumulo.core.clientImpl.ClientContext;
+import org.apache.accumulo.core.data.Key;
+import org.apache.accumulo.shell.Shell;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.Options;
+import org.apache.hadoop.io.Text;
+import org.easymock.Capture;
 import org.junit.jupiter.api.Test;
 
 public class MergeCommandTest {
 
+  public static class TestMergeCommand extends MergeCommand {
+    @Override
+    int executeMerge(Shell shellState, String tableName, Text startRow, Text 
endRow, long size,
+        boolean verbose, boolean force, boolean dryRun)
+        throws AccumuloException, TableNotFoundException, 
AccumuloSecurityException {
+      if (dryRun) {
+        shellState.getWriter()
+            .println(String.format(
+                "dry-run would have started a Fate Merge for table %s tablet 
range (%s to %s]",
+                tableName,
+                startRow == null ? "-inf"
+                    : Key.toPrintableString(startRow.getBytes(), 0, 
startRow.getLength(),
+                        startRow.getLength()),
+                endRow == null ? "+inf" : 
Key.toPrintableString(endRow.getBytes(), 0,
+                    endRow.getLength(), endRow.getLength())));
+        return 0;
+      }
+      if (size > 0) {
+        shellState.getAccumuloClient().tableOperations().merge(tableName, 
startRow, endRow);
+      }
+      return 0;
+    }
+  }
+
   @Test
   public void testBeginRowHelp() {
     assertTrue(
@@ -31,4 +80,68 @@ public class MergeCommandTest {
         "-b should say it is exclusive");
   }
 
+  @Test
+  public void mockDryRunMergeTest() throws Exception {
+    MergeCommand cmd = new TestMergeCommand();
+
+    AccumuloClient client = createMock(AccumuloClient.class);
+    ClientContext context = createMock(ClientContext.class);
+    TableOperations tableOps = createMock(TableOperations.class);
+    InstanceOperations instOps = createMock(InstanceOperations.class);
+    Shell shellState = createMock(Shell.class);
+    PrintWriter pw = createMock(PrintWriter.class);
+
+    Capture<String> capture = newCapture();
+    pw.println(capture(capture));
+
+    Options opts = cmd.getOptions();
+
+    CommandLineParser parser = new DefaultParser();
+    String[] args = {"-t", "testTable", "-s", "10G", "--dry-run"};
+    CommandLine cli = parser.parse(opts, args);
+
+    expect(shellState.getAccumuloClient()).andReturn(client).anyTimes();
+    expect(shellState.getContext()).andReturn(context).anyTimes();
+    expect(shellState.isVerbose()).andReturn(false).anyTimes();
+    expect(client.tableOperations()).andReturn(tableOps).anyTimes();
+    expect(tableOps.exists("testTable")).andReturn(true).anyTimes();
+    expect(shellState.getWriter()).andReturn(pw);
+
+    replay(client, context, tableOps, instOps, shellState, pw);
+    cmd.execute("merge", cli, shellState);
+    verify(client, context, tableOps, instOps, shellState, pw);
+    assertEquals(
+        "dry-run would have started a Fate Merge for table testTable tablet 
range (-inf to +inf]",
+        capture.getValue());
+  }
+
+  @Test
+  public void mockMergeAllTabletsTest() throws Exception {
+    MergeCommand cmd = new TestMergeCommand();
+
+    AccumuloClient client = createMock(AccumuloClient.class);
+    ClientContext context = createMock(ClientContext.class);
+    TableOperations tableOps = createMock(TableOperations.class);
+    InstanceOperations instOps = createMock(InstanceOperations.class);
+    Shell shellState = createMock(Shell.class);
+
+    Options opts = cmd.getOptions();
+
+    CommandLineParser parser = new DefaultParser();
+    String[] args = {"-t", "testTable"};
+    CommandLine cli = parser.parse(opts, args);
+
+    expect(shellState.getAccumuloClient()).andReturn(client).anyTimes();
+    expect(shellState.getContext()).andReturn(context).anyTimes();
+    expect(shellState.isVerbose()).andReturn(false).anyTimes();
+    expect(client.tableOperations()).andReturn(tableOps).anyTimes();
+    expect(tableOps.exists("testTable")).andReturn(true).anyTimes();
+    expect(shellState.confirm(
+        " Warning!!! Are you REALLY sure you want to merge the entire table { 
testTable } into one tablet?!?!?!"))
+        .andReturn(Optional.of(true)).once();
+
+    replay(client, context, tableOps, instOps, shellState);
+    cmd.execute("merge", cli, shellState);
+    verify(client, context, tableOps, instOps, shellState);
+  }
 }
diff --git 
a/test/src/main/java/org/apache/accumulo/test/functional/MergeIT.java 
b/test/src/main/java/org/apache/accumulo/test/functional/MergeIT.java
index 43590e8030..37a057e282 100644
--- a/test/src/main/java/org/apache/accumulo/test/functional/MergeIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/functional/MergeIT.java
@@ -21,9 +21,12 @@ package org.apache.accumulo.test.functional;
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashSet;
@@ -45,6 +48,8 @@ import org.apache.accumulo.core.data.Mutation;
 import org.apache.accumulo.core.data.TableId;
 import org.apache.accumulo.core.data.Value;
 import org.apache.accumulo.core.dataImpl.KeyExtent;
+import org.apache.accumulo.core.metadata.MetadataTable;
+import org.apache.accumulo.core.metadata.RootTable;
 import org.apache.accumulo.core.metadata.StoredTabletFile;
 import org.apache.accumulo.core.metadata.TabletFile;
 import org.apache.accumulo.core.metadata.schema.ExternalCompactionId;
@@ -109,10 +114,10 @@ public class MergeIT extends AccumuloClusterHarness {
       }
       c.tableOperations().flush(tableName, null, null, true);
       Merge merge = new Merge();
-      merge.mergomatic(c, tableName, null, null, 100, false);
+      merge.mergomatic(c, tableName, null, null, 100, false, false);
       assertArrayEquals("b c d e f x y".split(" "),
           toStrings(c.tableOperations().listSplits(tableName)));
-      merge.mergomatic(c, tableName, null, null, 100, true);
+      merge.mergomatic(c, tableName, null, null, 100, true, false);
       assertArrayEquals("c e f y".split(" "), 
toStrings(c.tableOperations().listSplits(tableName)));
     }
   }
@@ -278,4 +283,70 @@ public class MergeIT extends AccumuloClusterHarness {
       }
     }
   }
+
+  @Test
+  public void testDryRunMerge() throws Exception {
+    try (AccumuloClient c = 
Accumulo.newClient().from(getClientProps()).build()) {
+      String tableName = getUniqueNames(1)[0];
+      c.tableOperations().create(tableName);
+      c.tableOperations().addSplits(tableName,
+          new TreeSet<>(List.of(new Text("row0"), new Text("row1"), new 
Text("row10"))));
+      try (BatchWriter writer = c.createBatchWriter(tableName)) {
+        for (int i = 0; i < 100; i++) {
+          Mutation m = new Mutation(String.format("row00%d", i));
+          m.put("cf", "cq", "Test Value");
+          writer.addMutation(m);
+        }
+      }
+      c.tableOperations().flush(tableName, null, null, true);
+
+      TableId tableId = getServerContext().getTableId(tableName);
+      try (var tablets = 
getServerContext().getAmple().readTablets().forTable(tableId).build()) {
+        assertEquals(4, tablets.stream().count());
+      }
+
+      List<String> args = new ArrayList<>(List.of("-t", tableName, 
"--dry-run", "-s", "1G"));
+      getClientProps().stringPropertyNames().forEach(keyProp -> {
+        args.add("-o");
+        args.add(keyProp + "=" + getClientProps().getProperty(keyProp));
+      });
+      Merge.main(args.toArray(String[]::new));
+      try (var tablets = 
getServerContext().getAmple().readTablets().forTable(tableId).build()) {
+        assertEquals(4, tablets.stream().count());
+      }
+    }
+  }
+
+  @Test
+  public void testMetadataTableDoesNotMerge() {
+    List<String> args = new ArrayList<>(List.of("-t", MetadataTable.NAME, 
"-e", "~", "-s", "1"));
+    getClientProps().stringPropertyNames().forEach(keyProp -> {
+      args.add("-o");
+      args.add(keyProp + "=" + getClientProps().getProperty(keyProp));
+    });
+    Throwable ex =
+        assertThrows(Merge.MergeException.class, () -> 
Merge.main(args.toArray(String[]::new)));
+    assertInstanceOf(IllegalArgumentException.class, ex.getCause());
+    assertEquals(
+        "java.lang.IllegalArgumentException: cannot merge tablets on the 
accumulo.metadata table",
+        ex.getMessage());
+  }
+
+  @Test
+  public void testRootTableDoesNotMerge() {
+    List<String> args = new ArrayList<>(List.of("-t", RootTable.NAME));
+    getClientProps().stringPropertyNames().forEach(keyProp -> {
+      args.add("-o");
+      args.add(keyProp + "=" + getClientProps().getProperty(keyProp));
+    });
+    Exception e =
+        assertThrows(Merge.MergeException.class, () -> 
Merge.main(args.toArray(String[]::new)));
+    assertInstanceOf(IllegalArgumentException.class, e.getCause());
+    assertEquals(
+        "java.lang.IllegalArgumentException: cannot merge tablets on the 
accumulo.root table",
+        e.getMessage());
+    try (var tablets = 
getServerContext().getAmple().readTablets().forTable(RootTable.ID).build()) {
+      assertEquals(1, tablets.stream().count());
+    }
+  }
 }

Reply via email to