You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by pr...@apache.org on 2018/05/23 18:05:46 UTC

[geode] branch develop updated: GEODE-4858: Refactor 'list async-event-queue' command and function (#1975)

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

prhomberg pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to refs/heads/develop by this push:
     new 2faf3fb  GEODE-4858: Refactor 'list async-event-queue' command and function (#1975)
2faf3fb is described below

commit 2faf3fb7756109a27359f68de258164352709c40
Author: Patrick Rhomberg <pr...@pivotal.io>
AuthorDate: Wed May 23 11:05:41 2018 -0700

    GEODE-4858: Refactor 'list async-event-queue' command and function (#1975)
    
    * Command refactored for cleaner presentation
    * Command refactored to return ResultModel
    * Function refactored to extend CliFunction
---
 .../cli/commands/ListAsyncEventQueuesCommand.java  | 143 +++++------
 .../cli/domain/AsyncEventQueueDetails.java         |   6 +-
 .../functions/ListAsyncEventQueuesFunction.java    |  71 ++----
 .../ListAsyncEventQueuesCommandDUnitTest.java      |   7 +-
 .../cli/commands/ListAsyncEventQueuesTest.java     | 264 +++++++++++++++++++++
 .../test/junit/assertions/CommandResultAssert.java |   6 +-
 6 files changed, 368 insertions(+), 129 deletions(-)

diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesCommand.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesCommand.java
index 55fe663..c414307 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesCommand.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesCommand.java
@@ -15,104 +15,105 @@
 
 package org.apache.geode.management.internal.cli.commands;
 
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.List;
-import java.util.Map;
 import java.util.Properties;
 import java.util.Set;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.shell.core.annotation.CliCommand;
 
-import org.apache.geode.SystemFailure;
-import org.apache.geode.cache.execute.ResultCollector;
 import org.apache.geode.distributed.DistributedMember;
-import org.apache.geode.management.cli.Result;
-import org.apache.geode.management.internal.cli.CliUtil;
 import org.apache.geode.management.internal.cli.domain.AsyncEventQueueDetails;
 import org.apache.geode.management.internal.cli.functions.CliFunctionResult;
 import org.apache.geode.management.internal.cli.functions.ListAsyncEventQueuesFunction;
 import org.apache.geode.management.internal.cli.i18n.CliStrings;
-import org.apache.geode.management.internal.cli.result.ResultBuilder;
-import org.apache.geode.management.internal.cli.result.TabularResultData;
+import org.apache.geode.management.internal.cli.result.model.ResultModel;
+import org.apache.geode.management.internal.cli.result.model.TabularResultModel;
 import org.apache.geode.management.internal.security.ResourceOperation;
 import org.apache.geode.security.ResourcePermission;
 
 public class ListAsyncEventQueuesCommand extends InternalGfshCommand {
+  private static final String[] DETAILS_OUTPUT_COLUMNS =
+      {"Member", "ID", "Batch Size", "Persistent", "Disk Store", "Max Memory", "Listener"};
+  private static final String ASYNC_EVENT_QUEUES_TABLE_SECTION = "Async Event Queues";
+  private static final String MEMBER_ERRORS_TABLE_SECTION = "Member Errors";
+
   @CliCommand(value = CliStrings.LIST_ASYNC_EVENT_QUEUES,
       help = CliStrings.LIST_ASYNC_EVENT_QUEUES__HELP)
   @ResourceOperation(resource = ResourcePermission.Resource.CLUSTER,
       operation = ResourcePermission.Operation.READ)
-  public Result listAsyncEventQueues() {
-    try {
-      TabularResultData tabularData = ResultBuilder.createTabularResultData();
-      boolean accumulatedData = false;
+  public ResultModel listAsyncEventQueues() {
+    Set<DistributedMember> targetMembers = getAllNormalMembers();
+    if (targetMembers.isEmpty()) {
+      return ResultModel.createError(CliStrings.NO_MEMBERS_FOUND_MESSAGE);
+    }
 
-      Set<DistributedMember> targetMembers = getAllNormalMembers();
+    // Each (successful) member returns a list of AsyncEventQueueDetails.
+    List<CliFunctionResult> results = executeAndGetFunctionResult(
+        new ListAsyncEventQueuesFunction(), new Object[] {}, targetMembers);
 
-      if (targetMembers.isEmpty()) {
-        return ResultBuilder.createUserErrorResult(CliStrings.NO_MEMBERS_FOUND_MESSAGE);
-      }
+    ResultModel result = buildAsyncEventQueueInfo(results);
 
-      ResultCollector<?, ?> rc = CliUtil.executeFunction(new ListAsyncEventQueuesFunction(),
-          new Object[] {}, targetMembers);
-      List<CliFunctionResult> results = CliFunctionResult.cleanResults((List<?>) rc.getResult());
+    // Report any explicit errors as well.
+    if (results.stream().anyMatch(r -> !r.isSuccessful())) {
+      TabularResultModel errors = result.addTable(MEMBER_ERRORS_TABLE_SECTION);
+      errors.setColumnHeader("Member", "Error");
+      results.stream().filter(r -> !r.isSuccessful()).forEach(errorResult -> errors
+          .addRow(errorResult.getMemberIdOrName(), errorResult.getStatusMessage()));
+    }
 
-      for (CliFunctionResult result : results) {
-        if (result.getThrowable() != null) {
-          tabularData.accumulate("Member", result.getMemberIdOrName());
-          tabularData.accumulate("Result", "ERROR: " + result.getThrowable().getClass().getName()
-              + ": " + result.getThrowable().getMessage());
-          accumulatedData = true;
-          tabularData.setStatus(Result.Status.ERROR);
-        } else {
-          AsyncEventQueueDetails[] details = (AsyncEventQueueDetails[]) result.getSerializables();
-          for (AsyncEventQueueDetails detail : details) {
-            tabularData.accumulate("Member", result.getMemberIdOrName());
-            tabularData.accumulate("ID", detail.getId());
-            tabularData.accumulate("Batch Size", detail.getBatchSize());
-            tabularData.accumulate("Persistent", detail.isPersistent());
-            tabularData.accumulate("Disk Store", detail.getDiskStoreName());
-            tabularData.accumulate("Max Memory", detail.getMaxQueueMemory());
+    return result;
+  }
 
-            Properties listenerProperties = detail.getListenerProperties();
-            if (listenerProperties == null || listenerProperties.size() == 0) {
-              tabularData.accumulate("Listener", detail.getListener());
-            } else {
-              StringBuilder propsStringBuilder = new StringBuilder();
-              propsStringBuilder.append('(');
-              boolean firstProperty = true;
-              for (Map.Entry<Object, Object> property : listenerProperties.entrySet()) {
-                if (!firstProperty) {
-                  propsStringBuilder.append(',');
-                } else {
-                  firstProperty = false;
-                }
-                propsStringBuilder.append(property.getKey()).append('=')
-                    .append(property.getValue());
-              }
-              propsStringBuilder.append(')');
+  /**
+   * @return An info result containing the table of AsyncEventQueueDetails.
+   *         If no details are found, returns an info result message indicating so.
+   */
+  private ResultModel buildAsyncEventQueueInfo(List<CliFunctionResult> results) {
+    if (results.stream().filter(CliFunctionResult::isSuccessful)
+        .noneMatch(r -> ((List<AsyncEventQueueDetails>) r.getResultObject()).size() > 0)) {
+      return ResultModel.createInfo(CliStrings.LIST_ASYNC_EVENT_QUEUES__NO_QUEUES_FOUND_MESSAGE);
+    }
 
-              tabularData.accumulate("Listener",
-                  detail.getListener() + propsStringBuilder.toString());
-            }
-            accumulatedData = true;
-          }
-        }
-      }
+    ResultModel result = new ResultModel();
+    TabularResultModel detailsTable = result.addTable(ASYNC_EVENT_QUEUES_TABLE_SECTION);
+    detailsTable.setColumnHeader(DETAILS_OUTPUT_COLUMNS);
 
-      if (!accumulatedData) {
-        return ResultBuilder
-            .createInfoResult(CliStrings.LIST_ASYNC_EVENT_QUEUES__NO_QUEUES_FOUND_MESSAGE);
-      }
+    results.stream().filter(CliFunctionResult::isSuccessful).forEach(successfulResult -> {
+      String memberName = successfulResult.getMemberIdOrName();
+      ((List<AsyncEventQueueDetails>) successfulResult.getResultObject())
+          .forEach(entry -> detailsTable.addRow(memberName, entry.getId(),
+              String.valueOf(entry.getBatchSize()), String.valueOf(entry.isPersistent()),
+              String.valueOf(entry.getDiskStoreName()), String.valueOf(entry.getMaxQueueMemory()),
+              getListenerEntry(entry)));
+    });
+    return result;
+  }
+
+  /**
+   * @return The class of the entry's listener. If the listener is parameterized, these parameters
+   *         are appended in a json format.
+   */
+  private String getListenerEntry(AsyncEventQueueDetails entry) {
+    return entry.getListener() + propertiesToString(entry.getListenerProperties());
+  }
 
-      return ResultBuilder.buildResult(tabularData);
-    } catch (VirtualMachineError e) {
-      SystemFailure.initiateFailure(e);
-      throw e;
-    } catch (Throwable th) {
-      SystemFailure.checkFailure();
-      return ResultBuilder.createGemFireErrorResult(
-          CliStrings.format(CliStrings.LIST_ASYNC_EVENT_QUEUES__ERROR_WHILE_LISTING_REASON_0,
-              new Object[] {th.getMessage()}));
+  /**
+   * @return A json format of the properties, or the empty string if the properties are empty.
+   */
+  static String propertiesToString(Properties props) {
+    if (props == null || props.isEmpty()) {
+      return "";
+    }
+    ObjectMapper mapper = new ObjectMapper();
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    try {
+      mapper.writeValue(baos, props);
+    } catch (IOException e) {
+      return e.getMessage();
     }
+    return baos.toString();
   }
 }
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/domain/AsyncEventQueueDetails.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/domain/AsyncEventQueueDetails.java
index 720fe6f..95f836d 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/domain/AsyncEventQueueDetails.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/domain/AsyncEventQueueDetails.java
@@ -14,15 +14,15 @@
  */
 package org.apache.geode.management.internal.cli.domain;
 
+import java.io.Serializable;
+import java.util.Properties;
+
 /**
  * Used to transfer information about an AsyncEventQueue from a function being executed on a server
  * back to the manager that invoked the function.
  *
  * @since GemFire 8.0
  */
-import java.io.Serializable;
-import java.util.Properties;
-
 public class AsyncEventQueueDetails implements Serializable {
   private static final long serialVersionUID = 1L;
   private final String id;
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListAsyncEventQueuesFunction.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListAsyncEventQueuesFunction.java
index 2bb4a50..b0bd9de 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListAsyncEventQueuesFunction.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListAsyncEventQueuesFunction.java
@@ -14,21 +14,18 @@
  */
 package org.apache.geode.management.internal.cli.functions;
 
+import java.util.List;
 import java.util.Properties;
 import java.util.Set;
+import java.util.stream.Collectors;
 
-import org.apache.logging.log4j.Logger;
-
-import org.apache.geode.SystemFailure;
 import org.apache.geode.cache.Cache;
-import org.apache.geode.cache.CacheClosedException;
 import org.apache.geode.cache.asyncqueue.AsyncEventListener;
 import org.apache.geode.cache.asyncqueue.AsyncEventQueue;
 import org.apache.geode.cache.execute.FunctionContext;
 import org.apache.geode.distributed.DistributedMember;
-import org.apache.geode.internal.cache.execute.InternalFunction;
 import org.apache.geode.internal.cache.xmlcache.Declarable2;
-import org.apache.geode.internal.logging.LogService;
+import org.apache.geode.management.cli.CliFunction;
 import org.apache.geode.management.internal.cli.domain.AsyncEventQueueDetails;
 
 /**
@@ -38,8 +35,7 @@ import org.apache.geode.management.internal.cli.domain.AsyncEventQueueDetails;
  *
  * @since GemFire 8.0
  */
-public class ListAsyncEventQueuesFunction implements InternalFunction {
-  private static final Logger logger = LogService.getLogger();
+public class ListAsyncEventQueuesFunction extends CliFunction {
 
   private static final long serialVersionUID = 1L;
 
@@ -49,53 +45,26 @@ public class ListAsyncEventQueuesFunction implements InternalFunction {
   }
 
   @Override
-  public void execute(final FunctionContext context) {
-    // Declared here so that it's available when returning a Throwable
-    String memberId = "";
-
-    try {
-      Cache cache = context.getCache();
+  public CliFunctionResult executeFunction(final FunctionContext context) {
+    Cache cache = context.getCache();
+    DistributedMember member = cache.getDistributedSystem().getDistributedMember();
 
-      DistributedMember member = cache.getDistributedSystem().getDistributedMember();
-
-      memberId = member.getId();
-      // If they set a name use it instead
-      if (!member.getName().equals("")) {
-        memberId = member.getName();
-      }
+    // Identify by name if the name is non-trivial. Otherwise, use the ID
+    String memberId = !member.getName().equals("") ? member.getName() : member.getId();
 
-      Set<AsyncEventQueue> asyncEventQueues = cache.getAsyncEventQueues();
+    Set<AsyncEventQueue> asyncEventQueues = cache.getAsyncEventQueues();
 
-      AsyncEventQueueDetails[] asyncEventQueueDetails =
-          new AsyncEventQueueDetails[asyncEventQueues.size()];
-      int i = 0;
-      for (AsyncEventQueue queue : asyncEventQueues) {
-        AsyncEventListener listener = queue.getAsyncEventListener();
-        Properties listenerProperties = new Properties();
-        if (listener instanceof Declarable2) {
-          listenerProperties = ((Declarable2) listener).getConfig();
-        }
-        asyncEventQueueDetails[i++] = new AsyncEventQueueDetails(queue.getId(),
-            queue.getBatchSize(), queue.isPersistent(), queue.getDiskStoreName(),
-            queue.getMaximumQueueMemory(), listener.getClass().getName(), listenerProperties);
+    List<AsyncEventQueueDetails> details = asyncEventQueues.stream().map(queue -> {
+      AsyncEventListener listener = queue.getAsyncEventListener();
+      Properties listenerProperties = new Properties();
+      if (listener instanceof Declarable2) {
+        listenerProperties = ((Declarable2) listener).getConfig();
       }
+      return new AsyncEventQueueDetails(queue.getId(), queue.getBatchSize(), queue.isPersistent(),
+          queue.getDiskStoreName(), queue.getMaximumQueueMemory(), listener.getClass().getName(),
+          listenerProperties);
+    }).collect(Collectors.toList());
 
-      CliFunctionResult result = new CliFunctionResult(memberId, asyncEventQueueDetails);
-      context.getResultSender().lastResult(result);
-
-    } catch (CacheClosedException cce) {
-      CliFunctionResult result = new CliFunctionResult(memberId, false, null);
-      context.getResultSender().lastResult(result);
-
-    } catch (VirtualMachineError e) {
-      SystemFailure.initiateFailure(e);
-      throw e;
-
-    } catch (Throwable th) {
-      SystemFailure.checkFailure();
-      logger.error("Could not list async event queues: {}", th.getMessage(), th);
-      CliFunctionResult result = new CliFunctionResult(memberId, th, null);
-      context.getResultSender().lastResult(result);
-    }
+    return new CliFunctionResult(memberId, details);
   }
 }
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesCommandDUnitTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesCommandDUnitTest.java
index 38cea6c..7c6844d 100644
--- a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesCommandDUnitTest.java
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesCommandDUnitTest.java
@@ -24,6 +24,7 @@ import org.junit.Test;
 import org.junit.experimental.categories.Category;
 
 import org.apache.geode.internal.cache.wan.MyAsyncEventListener;
+import org.apache.geode.management.internal.cli.i18n.CliStrings;
 import org.apache.geode.test.dunit.rules.ClusterStartupRule;
 import org.apache.geode.test.dunit.rules.MemberVM;
 import org.apache.geode.test.junit.categories.AEQTest;
@@ -43,16 +44,16 @@ public class ListAsyncEventQueuesCommandDUnitTest {
   private static MemberVM locator;
 
   @BeforeClass
-  public static void beforeClass() throws Exception {
+  public static void beforeClass() {
     locator = lsRule.startLocatorVM(0);
     lsRule.startServerVM(1, "group1", locator.getPort());
     lsRule.startServerVM(2, "group2", locator.getPort());
   }
 
   @Test
-  public void list() throws Exception {
+  public void list() {
     gfsh.executeAndAssertThat("list async-event-queue").statusIsSuccess()
-        .containsOutput("No Async Event Queues Found");
+        .containsOutput(CliStrings.LIST_ASYNC_EVENT_QUEUES__NO_QUEUES_FOUND_MESSAGE);
 
     gfsh.executeAndAssertThat("create async-event-queue --id=queue1 --group=group1 --listener="
         + MyAsyncEventListener.class.getName()).statusIsSuccess();
diff --git a/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesTest.java b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesTest.java
new file mode 100644
index 0000000..a771a57
--- /dev/null
+++ b/geode-core/src/test/java/org/apache/geode/management/internal/cli/commands/ListAsyncEventQueuesTest.java
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.apache.geode.management.internal.cli.commands;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.management.internal.cli.domain.AsyncEventQueueDetails;
+import org.apache.geode.management.internal.cli.functions.CliFunctionResult;
+import org.apache.geode.management.internal.cli.i18n.CliStrings;
+import org.apache.geode.test.junit.categories.AEQTest;
+import org.apache.geode.test.junit.categories.UnitTest;
+import org.apache.geode.test.junit.rules.GfshParserRule;
+
+@Category({UnitTest.class, AEQTest.class})
+public class ListAsyncEventQueuesTest {
+  private static final String COMMAND = "list async-event-queues ";
+
+  @ClassRule
+  public static GfshParserRule gfsh = new GfshParserRule();
+  private static final String FUNCTION_EXCEPTION_MESSAGE =
+      "A mysterious test exception occurred during function execution.";
+
+  private ListAsyncEventQueuesCommand command;
+  private List<CliFunctionResult> memberCliResults;
+  private DistributedMember mockedMember;
+  private DistributedMember anotherMockedMember;
+
+
+  @Before
+  public void before() throws Exception {
+    mockedMember = mock(DistributedMember.class);
+    anotherMockedMember = mock(DistributedMember.class);
+    command = spy(ListAsyncEventQueuesCommand.class);
+  }
+
+  @Test
+  public void noMembersFound() {
+    // Mock zero members
+    doReturn(Collections.emptySet()).when(command).getAllNormalMembers();
+
+    // Command should succeed with one row of data
+    gfsh.executeAndAssertThat(command, COMMAND).statusIsError()
+        .containsOutput(CliStrings.NO_MEMBERS_FOUND_MESSAGE);
+  }
+
+  @Test
+  public void oneServerWithOneQueue() {
+    // Mock one member
+    doReturn(new HashSet<>(Collections.singletonList(mockedMember))).when(command)
+        .getAllNormalMembers();
+
+    // Mock member's queue details
+    FakeDetails details = new FakeDetails("server1", "s1-queue-id", 5, true, "diskStoreName", 10,
+        "my.listener.class", new Properties());
+    CliFunctionResult memberResult = new CliFunctionResult(details.getMemberName(),
+        Collections.singletonList(details.asAsyncEventQueueDetails()));
+    memberCliResults = Collections.singletonList(memberResult);
+    doReturn(memberCliResults).when(command).executeAndGetFunctionResult(any(), any(), any());
+
+    // Command should succeed with one row of data
+    gfsh.executeAndAssertThat(command, COMMAND).statusIsSuccess()
+        .tableHasRowWithValues(details.expectedRowHeaderAndValue());
+  }
+
+  @Test
+  public void oneServerWithOneParameterizedQueue() {
+    // Mock one member
+    doReturn(new HashSet<>(Collections.singletonList(mockedMember))).when(command)
+        .getAllNormalMembers();
+
+    // Mock member's queue details
+    Properties listenerProperties = new Properties();
+    listenerProperties.setProperty("special-property", "special-value");
+    listenerProperties.setProperty("another-property", "mundane-value");
+    FakeDetails details = new FakeDetails("server1", "s1-queue-id", 5, true, "diskStoreName", 10,
+        "my.listener.class", listenerProperties);
+    CliFunctionResult memberResult = new CliFunctionResult(details.getMemberName(),
+        Collections.singletonList(details.asAsyncEventQueueDetails()));
+    memberCliResults = Collections.singletonList(memberResult);
+    doReturn(memberCliResults).when(command).executeAndGetFunctionResult(any(), any(), any());
+
+    // Command should succeed with one row of data
+    gfsh.executeAndAssertThat(command, COMMAND).statusIsSuccess()
+        .tableHasRowWithValues(details.expectedRowHeaderAndValue());
+  }
+
+  @Test
+  public void oneMemberButNoQueues() {
+    // Mock one member
+    doReturn(new HashSet<>(Collections.singletonList(mockedMember))).when(command)
+        .getAllNormalMembers();
+
+    // Mock member's lack of queue details
+    CliFunctionResult memberResult = new CliFunctionResult("server1", Collections.emptyList());
+    memberCliResults = Collections.singletonList(memberResult);
+    doReturn(memberCliResults).when(command).executeAndGetFunctionResult(any(), any(), any());
+
+    // Command should succeed, but indicate there were no queues
+    gfsh.executeAndAssertThat(command, COMMAND).statusIsSuccess()
+        .containsOutput(CliStrings.LIST_ASYNC_EVENT_QUEUES__NO_QUEUES_FOUND_MESSAGE);
+  }
+
+  @Test
+  public void oneMemberWhoErrorsOut() {
+    // Mock one member
+    doReturn(new HashSet<>(Collections.singletonList(mockedMember))).when(command)
+        .getAllNormalMembers();
+
+    // Mock member's error result
+    CliFunctionResult memberResult =
+        new CliFunctionResult("server1", new Exception(FUNCTION_EXCEPTION_MESSAGE));
+    memberCliResults = Collections.singletonList(memberResult);
+    doReturn(memberCliResults).when(command).executeAndGetFunctionResult(any(), any(), any());
+
+    // Command should succeed, but indicate there were no queues
+    gfsh.executeAndAssertThat(command, COMMAND).statusIsSuccess()
+        .containsOutput(CliStrings.LIST_ASYNC_EVENT_QUEUES__NO_QUEUES_FOUND_MESSAGE);
+  }
+
+  @Test
+  public void twoServersWithManyQueues() {
+    // Mock two members, even though they're not directly consumed
+    doReturn(new HashSet<>(Arrays.asList(mockedMember, anotherMockedMember))).when(command)
+        .getAllNormalMembers();
+
+    // Mock member's queue details
+    FakeDetails details1 = new FakeDetails("server1", "s1-queue-id1", 5, false, "diskStoreName", 1,
+        "my.listener.class", new Properties());
+    FakeDetails details2 = new FakeDetails("server1", "s1-queue-id2", 15, true,
+        "otherDiskStoreName", 10, "my.listener.class", new Properties());
+    FakeDetails details3 = new FakeDetails("server1", "s1-queue-id3", 25, true, "diskStoreName",
+        100, "my.listener.class", new Properties());
+    CliFunctionResult member1Result =
+        new CliFunctionResult("server1", Arrays.asList(details1.asAsyncEventQueueDetails(),
+            details2.asAsyncEventQueueDetails(), details3.asAsyncEventQueueDetails()));
+
+    FakeDetails details4 = new FakeDetails("server2", "s2-queue-id1", 5, false, "diskStoreName", 1,
+        "my.listener.class", new Properties());
+    FakeDetails details5 = new FakeDetails("server2", "s2-queue-id2", 15, true,
+        "otherDiskStoreName", 10, "my.listener.class", new Properties());
+    FakeDetails details6 = new FakeDetails("server2", "s2-queue-id3", 25, true, "diskStoreName",
+        100, "my.listener.class", new Properties());
+    CliFunctionResult member2Result =
+        new CliFunctionResult("server2", Arrays.asList(details4.asAsyncEventQueueDetails(),
+            details5.asAsyncEventQueueDetails(), details6.asAsyncEventQueueDetails()));
+
+    memberCliResults = Arrays.asList(member1Result, member2Result);
+    doReturn(memberCliResults).when(command).executeAndGetFunctionResult(any(), any(), any());
+
+    // Command should succeed with one row of data
+    gfsh.executeAndAssertThat(command, COMMAND).statusIsSuccess()
+        .tableHasRowWithValues(details1.expectedRowHeaderAndValue())
+        .tableHasRowWithValues(details2.expectedRowHeaderAndValue())
+        .tableHasRowWithValues(details3.expectedRowHeaderAndValue())
+        .tableHasRowWithValues(details4.expectedRowHeaderAndValue())
+        .tableHasRowWithValues(details5.expectedRowHeaderAndValue())
+        .tableHasRowWithValues(details6.expectedRowHeaderAndValue());
+  }
+
+  @Test
+  public void oneMemberSucceedsAndOneFails() {
+    // Mock two members, even though they're not directly consumed
+    doReturn(new HashSet<>(Arrays.asList(mockedMember, anotherMockedMember))).when(command)
+        .getAllNormalMembers();
+
+    // Mock member's queue details
+    FakeDetails details1 = new FakeDetails("server1", "s1-queue-id1", 5, false, "diskStoreName", 1,
+        "my.listener.class", new Properties());
+    FakeDetails details2 = new FakeDetails("server1", "s1-queue-id2", 15, true,
+        "otherDiskStoreName", 10, "my.listener.class", new Properties());
+    FakeDetails details3 = new FakeDetails("server1", "s1-queue-id3", 25, true, "diskStoreName",
+        100, "my.listener.class", new Properties());
+    CliFunctionResult member1Result =
+        new CliFunctionResult("server1", Arrays.asList(details1.asAsyncEventQueueDetails(),
+            details2.asAsyncEventQueueDetails(), details3.asAsyncEventQueueDetails()));
+
+    // Mock the other's failure
+    CliFunctionResult member2Result =
+        new CliFunctionResult("server2", new Exception(FUNCTION_EXCEPTION_MESSAGE));
+
+    memberCliResults = Arrays.asList(member1Result, member2Result);
+    doReturn(memberCliResults).when(command).executeAndGetFunctionResult(any(), any(), any());
+
+    // Command should succeed with one row of data
+    gfsh.executeAndAssertThat(command, COMMAND).statusIsSuccess()
+        .tableHasRowWithValues(details1.expectedRowHeaderAndValue())
+        .tableHasRowWithValues(details2.expectedRowHeaderAndValue())
+        .tableHasRowWithValues(details3.expectedRowHeaderAndValue())
+        .containsOutput(FUNCTION_EXCEPTION_MESSAGE);
+  }
+
+  /**
+   * Wrapper for mocked AsyncEventQueueData, with convenience method for expected table output.
+   */
+  class FakeDetails {
+    private String memberName;
+    private String queueId;
+    private int batchSize;
+    private boolean persistent;
+    private String diskStoreName;
+    private int maxQueueMemory;
+    private String listener;
+    private Properties listenerProperties;
+
+    private FakeDetails(String memberName, String queueId, int batchSize, boolean persistent,
+        String diskStoreName, int maxQueueMemory, String listener, Properties listenerProperties) {
+      this.memberName = memberName;
+      this.queueId = queueId;
+      this.batchSize = batchSize;
+      this.persistent = persistent;
+      this.diskStoreName = diskStoreName;
+      this.maxQueueMemory = maxQueueMemory;
+      this.listener = listener;
+      this.listenerProperties = listenerProperties;
+    }
+
+    public String getMemberName() {
+      return memberName;
+    }
+
+    private AsyncEventQueueDetails asAsyncEventQueueDetails() {
+      return new AsyncEventQueueDetails(queueId, batchSize, persistent, diskStoreName,
+          maxQueueMemory, listener, listenerProperties);
+    }
+
+    private String[] expectedRowHeaderAndValue() {
+      return new String[] {"Member", "ID", "Batch Size", "Persistent", "Disk Store", "Max Memory",
+          "Listener", memberName, queueId, String.valueOf(batchSize), String.valueOf(persistent),
+          diskStoreName, String.valueOf(maxQueueMemory), expectedListenerOutput()};
+    }
+
+    private String expectedListenerOutput() {
+      return listener + ListAsyncEventQueuesCommand.propertiesToString(listenerProperties);
+    }
+  }
+}
diff --git a/geode-core/src/test/java/org/apache/geode/test/junit/assertions/CommandResultAssert.java b/geode-core/src/test/java/org/apache/geode/test/junit/assertions/CommandResultAssert.java
index 7528c0b..cb25cbc 100644
--- a/geode-core/src/test/java/org/apache/geode/test/junit/assertions/CommandResultAssert.java
+++ b/geode-core/src/test/java/org/apache/geode/test/junit/assertions/CommandResultAssert.java
@@ -21,6 +21,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.lang.StringUtils;
 import org.assertj.core.api.AbstractAssert;
 import org.assertj.core.api.Assertions;
 import org.json.JSONArray;
@@ -217,7 +218,10 @@ public class CommandResultAssert
     }
 
     // did not find any matching rows, then this would pass only if we do not pass in any values
-    assertThat(headersThenValues.length).describedAs("No matching row found.").isEqualTo(0);
+    assertThat(headersThenValues.length)
+        .describedAs("No matching row found containing expected values: "
+            + StringUtils.join(expectedValues, ","))
+        .isEqualTo(0);
     return this;
   }
 

-- 
To stop receiving notification emails like this one, please contact
prhomberg@apache.org.