2 * Copyright (c) 2014 Brocade Communications Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.netconf.sal.connect.netconf.listener;
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assert.assertFalse;
12 import static org.junit.Assert.assertNotNull;
13 import static org.junit.Assert.assertTrue;
14 import static org.mockito.ArgumentMatchers.any;
15 import static org.mockito.ArgumentMatchers.eq;
16 import static org.mockito.ArgumentMatchers.same;
17 import static org.mockito.Mockito.doNothing;
18 import static org.mockito.Mockito.doReturn;
19 import static org.mockito.Mockito.mock;
20 import static org.mockito.Mockito.never;
21 import static org.mockito.Mockito.reset;
22 import static org.mockito.Mockito.spy;
23 import static org.mockito.Mockito.timeout;
24 import static org.mockito.Mockito.verify;
25 import static org.opendaylight.netconf.api.xml.XmlNetconfConstants.URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0;
27 import com.google.common.base.CharMatcher;
28 import com.google.common.base.Strings;
29 import com.google.common.util.concurrent.ListenableFuture;
30 import io.netty.channel.ChannelFuture;
31 import io.netty.channel.EventLoopGroup;
32 import io.netty.channel.nio.NioEventLoopGroup;
33 import io.netty.util.HashedWheelTimer;
34 import io.netty.util.Timer;
35 import io.netty.util.concurrent.Future;
36 import io.netty.util.concurrent.GenericFutureListener;
37 import io.netty.util.concurrent.GlobalEventExecutor;
38 import java.io.ByteArrayInputStream;
39 import java.net.InetSocketAddress;
40 import java.util.ArrayList;
41 import java.util.Collections;
43 import java.util.UUID;
44 import java.util.concurrent.TimeUnit;
45 import java.util.concurrent.TimeoutException;
46 import javax.xml.parsers.ParserConfigurationException;
47 import org.junit.Before;
48 import org.junit.Test;
49 import org.junit.runner.RunWith;
50 import org.mockito.ArgumentCaptor;
51 import org.mockito.Mock;
52 import org.mockito.junit.MockitoJUnitRunner;
53 import org.opendaylight.netconf.api.NetconfMessage;
54 import org.opendaylight.netconf.api.NetconfTerminationReason;
55 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
56 import org.opendaylight.netconf.client.NetconfClientDispatcherImpl;
57 import org.opendaylight.netconf.client.NetconfClientSession;
58 import org.opendaylight.netconf.client.conf.NetconfClientConfiguration;
59 import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfiguration;
60 import org.opendaylight.netconf.client.conf.NetconfReconnectingClientConfigurationBuilder;
61 import org.opendaylight.netconf.nettyutil.ReconnectStrategy;
62 import org.opendaylight.netconf.nettyutil.TimedReconnectStrategy;
63 import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.LoginPasswordHandler;
64 import org.opendaylight.netconf.sal.connect.api.RemoteDevice;
65 import org.opendaylight.netconf.sal.connect.api.RemoteDeviceId;
66 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil;
67 import org.opendaylight.yangtools.util.xml.UntrustedXML;
68 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
69 import org.opendaylight.yangtools.yang.common.ErrorTag;
70 import org.opendaylight.yangtools.yang.common.ErrorType;
71 import org.opendaylight.yangtools.yang.common.QName;
72 import org.opendaylight.yangtools.yang.common.RpcError;
73 import org.opendaylight.yangtools.yang.common.RpcResult;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
76 import org.w3c.dom.Document;
77 import org.w3c.dom.Element;
79 @RunWith(MockitoJUnitRunner.StrictStubs.class)
80 public class NetconfDeviceCommunicatorTest {
82 private static final Logger LOG = LoggerFactory.getLogger(NetconfDeviceCommunicatorTest.class);
85 NetconfClientSession mockSession;
88 RemoteDevice<NetconfDeviceCommunicator> mockDevice;
90 NetconfDeviceCommunicator communicator;
93 public void setUp() throws Exception {
94 communicator = new NetconfDeviceCommunicator(
95 new RemoteDeviceId("test", InetSocketAddress.createUnresolved("localhost", 22)), mockDevice, 10);
99 doReturn(Collections.<String>emptySet()).when(mockSession).getServerCapabilities();
100 doNothing().when(mockDevice).onRemoteSessionUp(any(NetconfSessionPreferences.class),
101 any(NetconfDeviceCommunicator.class));
102 communicator.onSessionUp(mockSession);
105 private ListenableFuture<RpcResult<NetconfMessage>> sendRequest() throws Exception {
106 return sendRequest(UUID.randomUUID().toString(), true);
109 @SuppressWarnings("unchecked")
110 private ListenableFuture<RpcResult<NetconfMessage>> sendRequest(final String messageID,
111 final boolean doLastTest) throws Exception {
112 Document doc = UntrustedXML.newDocumentBuilder().newDocument();
113 Element element = doc.createElement("request");
114 element.setAttribute("message-id", messageID);
115 doc.appendChild(element);
116 NetconfMessage message = new NetconfMessage(doc);
118 ChannelFuture mockChannelFuture = mock(ChannelFuture.class);
119 doReturn(mockChannelFuture).when(mockChannelFuture)
120 .addListener(any(GenericFutureListener.class));
121 doReturn(mockChannelFuture).when(mockSession).sendMessage(same(message));
123 ListenableFuture<RpcResult<NetconfMessage>> resultFuture =
124 communicator.sendRequest(message, QName.create("", "mockRpc"));
126 assertNotNull("ListenableFuture is null", resultFuture);
132 public void testOnSessionUp() {
133 final var testCapability = "urn:opendaylight:params:xml:ns:test?module=test-module&revision=2014-06-02";
134 final var serverCapabilities = Set.of(
135 NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString(),
136 NetconfMessageTransformUtil.IETF_NETCONF_MONITORING.getNamespace().toString(),
138 doReturn(serverCapabilities).when(mockSession).getServerCapabilities();
140 final var netconfSessionPreferences = ArgumentCaptor.forClass(NetconfSessionPreferences.class);
141 doNothing().when(mockDevice).onRemoteSessionUp(netconfSessionPreferences.capture(), eq(communicator));
143 communicator.onSessionUp(mockSession);
145 verify(mockSession).getServerCapabilities();
146 verify(mockDevice).onRemoteSessionUp(netconfSessionPreferences.capture(), eq(communicator));
148 NetconfSessionPreferences actualCapabilites = netconfSessionPreferences.getValue();
149 assertTrue(actualCapabilites.containsNonModuleCapability(
150 NetconfMessageTransformUtil.NETCONF_ROLLBACK_ON_ERROR_URI.toString()));
151 assertFalse(actualCapabilites.containsNonModuleCapability(testCapability));
152 assertEquals(Set.of(QName.create("urn:opendaylight:params:xml:ns:test", "2014-06-02", "test-module")),
153 actualCapabilites.moduleBasedCaps().keySet());
154 assertTrue(actualCapabilites.isRollbackSupported());
155 assertTrue(actualCapabilites.isMonitoringSupported());
158 @SuppressWarnings("unchecked")
159 @Test(timeout = 5000)
160 public void testOnSessionDown() throws Exception {
163 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest();
164 final ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest();
166 doNothing().when(mockDevice).onRemoteSessionDown();
168 communicator.onSessionDown(mockSession, new Exception("mock ex"));
170 verifyErrorRpcResult(resultFuture1.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
171 verifyErrorRpcResult(resultFuture2.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
173 verify(mockDevice).onRemoteSessionDown();
177 communicator.onSessionDown(mockSession, new Exception("mock ex"));
179 verify(mockDevice, never()).onRemoteSessionDown();
183 public void testOnSessionTerminated() throws Exception {
186 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest();
188 doNothing().when(mockDevice).onRemoteSessionDown();
190 String reasonText = "testing terminate";
191 NetconfTerminationReason reason = new NetconfTerminationReason(reasonText);
192 communicator.onSessionTerminated(mockSession, reason);
194 RpcError rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
195 assertEquals("RpcError message", reasonText, rpcError.getMessage());
197 verify(mockDevice).onRemoteSessionDown();
201 public void testClose() throws Exception {
202 communicator.close();
203 verify(mockDevice, never()).onRemoteSessionDown();
206 @SuppressWarnings({"rawtypes", "unchecked"})
208 public void testSendRequest() throws Exception {
211 NetconfMessage message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
212 QName rpc = QName.create("", "mockRpc");
214 ArgumentCaptor<GenericFutureListener> futureListener =
215 ArgumentCaptor.forClass(GenericFutureListener.class);
217 ChannelFuture mockChannelFuture = mock(ChannelFuture.class);
218 doReturn(mockChannelFuture).when(mockChannelFuture).addListener(futureListener.capture());
219 doReturn(mockChannelFuture).when(mockSession).sendMessage(same(message));
221 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest(message, rpc);
223 verify(mockSession).sendMessage(same(message));
225 assertNotNull("ListenableFuture is null", resultFuture);
227 verify(mockChannelFuture).addListener(futureListener.capture());
228 Future<Void> operationFuture = mock(Future.class);
229 doReturn(true).when(operationFuture).isSuccess();
230 futureListener.getValue().operationComplete(operationFuture);
233 resultFuture.get(1, TimeUnit.MILLISECONDS); // verify it's not cancelled or has an error set
234 } catch (TimeoutException e) {
235 LOG.info("Operation failed due timeout.");
240 public void testSendRequestWithNoSession() throws Exception {
241 NetconfMessage message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
242 QName rpc = QName.create("", "mockRpc");
244 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest(message, rpc);
246 assertNotNull("ListenableFuture is null", resultFuture);
248 // Should have an immediate result
249 RpcResult<NetconfMessage> rpcResult = resultFuture.get(3, TimeUnit.MILLISECONDS);
251 verifyErrorRpcResult(rpcResult, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
254 private static NetconfMessage createSuccessResponseMessage(final String messageID)
255 throws ParserConfigurationException {
256 Document doc = UntrustedXML.newDocumentBuilder().newDocument();
258 doc.createElementNS(URN_IETF_PARAMS_XML_NS_NETCONF_BASE_1_0, XmlNetconfConstants.RPC_REPLY_KEY);
259 rpcReply.setAttribute("message-id", messageID);
260 Element element = doc.createElementNS("ns", "data");
261 element.setTextContent(messageID);
262 rpcReply.appendChild(element);
263 doc.appendChild(rpcReply);
265 return new NetconfMessage(doc);
268 @SuppressWarnings({ "rawtypes", "unchecked" })
270 public void testSendRequestWithWithSendFailure() throws Exception {
273 NetconfMessage message = new NetconfMessage(UntrustedXML.newDocumentBuilder().newDocument());
274 QName rpc = QName.create("", "mockRpc");
276 ArgumentCaptor<GenericFutureListener> futureListener =
277 ArgumentCaptor.forClass(GenericFutureListener.class);
279 ChannelFuture mockChannelFuture = mock(ChannelFuture.class);
280 doReturn(mockChannelFuture).when(mockChannelFuture).addListener(futureListener.capture());
281 doReturn(mockChannelFuture).when(mockSession).sendMessage(same(message));
283 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = communicator.sendRequest(message, rpc);
285 assertNotNull("ListenableFuture is null", resultFuture);
287 verify(mockChannelFuture).addListener(futureListener.capture());
289 Future<Void> operationFuture = mock(Future.class);
290 doReturn(false).when(operationFuture).isSuccess();
291 doReturn(new Exception("mock error")).when(operationFuture).cause();
292 futureListener.getValue().operationComplete(operationFuture);
294 // Should have an immediate result
295 RpcResult<NetconfMessage> rpcResult = resultFuture.get(3, TimeUnit.MILLISECONDS);
297 RpcError rpcError = verifyErrorRpcResult(rpcResult, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED);
298 assertEquals("RpcError message contains \"mock error\"", true,
299 rpcError.getMessage().contains("mock error"));
302 //Test scenario verifying whether missing message is handled
304 public void testOnMissingResponseMessage() throws Exception {
308 String messageID1 = UUID.randomUUID().toString();
309 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest(messageID1, true);
311 String messageID2 = UUID.randomUUID().toString();
312 ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest(messageID2, true);
314 String messageID3 = UUID.randomUUID().toString();
315 ListenableFuture<RpcResult<NetconfMessage>> resultFuture3 = sendRequest(messageID3, true);
317 //response messages 1,2 are omitted
318 communicator.onMessage(mockSession, createSuccessResponseMessage(messageID3));
320 verifyResponseMessage(resultFuture3.get(), messageID3);
324 public void testOnSuccessfulResponseMessage() throws Exception {
327 String messageID1 = UUID.randomUUID().toString();
328 ListenableFuture<RpcResult<NetconfMessage>> resultFuture1 = sendRequest(messageID1, true);
330 String messageID2 = UUID.randomUUID().toString();
331 final ListenableFuture<RpcResult<NetconfMessage>> resultFuture2 = sendRequest(messageID2, true);
333 communicator.onMessage(mockSession, createSuccessResponseMessage(messageID1));
334 communicator.onMessage(mockSession, createSuccessResponseMessage(messageID2));
336 verifyResponseMessage(resultFuture1.get(), messageID1);
337 verifyResponseMessage(resultFuture2.get(), messageID2);
341 public void testOnResponseMessageWithError() throws Exception {
344 String messageID = UUID.randomUUID().toString();
345 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(messageID, true);
347 communicator.onMessage(mockSession, createErrorResponseMessage(messageID));
349 RpcError rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.RPC, ErrorTag.MISSING_ATTRIBUTE);
350 assertEquals("RpcError message", "Missing attribute", rpcError.getMessage());
352 String errorInfo = rpcError.getInfo();
353 assertNotNull("RpcError info is null", errorInfo);
354 assertTrue("Error info contains \"foo\"", errorInfo.contains("<bad-attribute>foo</bad-attribute>"));
355 assertTrue("Error info contains \"bar\"", errorInfo.contains("<bad-element>bar</bad-element>"));
359 public void testOnResponseMessageWithMultipleErrors() throws Exception {
362 String messageID = UUID.randomUUID().toString();
363 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(messageID, true);
365 communicator.onMessage(mockSession, createMultiErrorResponseMessage(messageID));
367 RpcError rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.PROTOCOL, ErrorTag.OPERATION_FAILED);
369 String errorInfo = rpcError.getInfo();
370 assertNotNull("RpcError info is null", errorInfo);
372 String errorInfoMessages = rpcError.getInfo();
373 String errMsg1 = "Number of member links configured, i.e [1], "
374 + "for interface [ae0]is lesser than the required minimum [2].";
375 String errMsg2 = "configuration check-out failed";
376 assertTrue(String.format("Error info contains \"%s\" or \"%s\'", errMsg1, errMsg2),
377 errorInfoMessages.contains(errMsg1) && errorInfoMessages.contains(errMsg2));
381 * Test whether reconnect is scheduled properly.
384 public void testNetconfDeviceReconnectInCommunicator() {
385 final RemoteDevice<NetconfDeviceCommunicator> device = mock(RemoteDevice.class);
387 final TimedReconnectStrategy timedReconnectStrategy =
388 new TimedReconnectStrategy(GlobalEventExecutor.INSTANCE, 10000, 0, 1.0, null, 100L, null);
389 final ReconnectStrategy reconnectStrategy = spy(new ReconnectStrategy() {
392 public int getConnectTimeout() throws Exception {
393 return timedReconnectStrategy.getConnectTimeout();
398 public Future<Void> scheduleReconnect(final Throwable cause) {
399 return timedReconnectStrategy.scheduleReconnect(cause);
404 public void reconnectSuccessful() {
405 timedReconnectStrategy.reconnectSuccessful();
409 final EventLoopGroup group = new NioEventLoopGroup();
410 final Timer time = new HashedWheelTimer();
412 final NetconfDeviceCommunicator listener = new NetconfDeviceCommunicator(
413 new RemoteDeviceId("test", InetSocketAddress.createUnresolved("localhost", 22)), device, 10);
414 final NetconfReconnectingClientConfiguration cfg = NetconfReconnectingClientConfigurationBuilder.create()
415 .withAddress(new InetSocketAddress("localhost", 65000))
416 .withReconnectStrategy(reconnectStrategy)
417 .withConnectStrategyFactory(() -> reconnectStrategy)
418 .withAuthHandler(new LoginPasswordHandler("admin", "admin"))
419 .withConnectionTimeoutMillis(10000)
420 .withProtocol(NetconfClientConfiguration.NetconfClientProtocol.SSH)
421 .withSessionListener(listener)
424 listener.initializeRemoteConnection(new NetconfClientDispatcherImpl(group, group, time), cfg);
426 verify(reconnectStrategy,
427 timeout(TimeUnit.MINUTES.toMillis(4)).times(101)).scheduleReconnect(any(Throwable.class));
430 group.shutdownGracefully();
435 public void testOnResponseMessageWithWrongMessageID() throws Exception {
438 String messageID = UUID.randomUUID().toString();
439 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(messageID, true);
441 communicator.onMessage(mockSession, createSuccessResponseMessage(UUID.randomUUID().toString()));
443 RpcError rpcError = verifyErrorRpcResult(resultFuture.get(), ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
444 assertFalse("RpcError message non-empty", Strings.isNullOrEmpty(rpcError.getMessage()));
446 String errorInfo = rpcError.getInfo();
447 assertNotNull("RpcError info is null", errorInfo);
448 assertTrue("Error info contains \"actual-message-id\"", errorInfo.contains("actual-message-id"));
449 assertTrue("Error info contains \"expected-message-id\"", errorInfo.contains("expected-message-id"));
453 public void testConcurrentMessageLimit() throws Exception {
455 ArrayList<String> messageID = new ArrayList<>();
457 for (int i = 0; i < 10; i++) {
458 messageID.add(UUID.randomUUID().toString());
459 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(messageID.get(i), false);
460 assertEquals("ListenableFuture is null", true, resultFuture instanceof UncancellableFuture);
463 final String notWorkingMessageID = UUID.randomUUID().toString();
464 ListenableFuture<RpcResult<NetconfMessage>> resultFuture = sendRequest(notWorkingMessageID, false);
465 assertEquals("ListenableFuture is null", false, resultFuture instanceof UncancellableFuture);
467 communicator.onMessage(mockSession, createSuccessResponseMessage(messageID.get(0)));
469 resultFuture = sendRequest(messageID.get(0), false);
470 assertNotNull("ListenableFuture is null", resultFuture);
473 private static NetconfMessage createMultiErrorResponseMessage(final String messageID) throws Exception {
474 // multiple rpc-errors which simulate actual response like in NETCONF-666
475 String xmlStr = "<nc:rpc-reply xmlns:nc=\"urn:ietf:params:xml:ns:netconf:base:1.0\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\" xmlns:junos=\"http://xml.juniper.net/junos/18.4R1/junos\""
476 + " message-id=\"" + messageID + "\">"
478 + "<nc:error-type>protocol</nc:error-type>\n"
479 + "<nc:error-tag>operation-failed</nc:error-tag>\n"
480 + "<nc:error-severity>error</nc:error-severity>\n"
481 + "<source-daemon>\n"
483 + "</source-daemon>\n"
484 + "<nc:error-message>\n"
485 + "Number of member links configured, i.e [1], "
486 + "for interface [ae0]is lesser than the required minimum [2].\n"
487 + "</nc:error-message>\n"
488 + "</nc:rpc-error>\n"
490 + "<nc:error-type>protocol</nc:error-type>\n"
491 + "<nc:error-tag>operation-failed</nc:error-tag>\n"
492 + "<nc:error-severity>error</nc:error-severity>\n"
493 + "<nc:error-message>\n"
494 + "configuration check-out failed\n"
495 + "</nc:error-message>\n"
496 + "</nc:rpc-error>\n"
499 ByteArrayInputStream bis = new ByteArrayInputStream(xmlStr.getBytes());
500 Document doc = UntrustedXML.newDocumentBuilder().parse(bis);
501 return new NetconfMessage(doc);
504 private static NetconfMessage createErrorResponseMessage(final String messageID) throws Exception {
505 String xmlStr = "<rpc-reply xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\""
506 + " message-id=\"" + messageID + "\">"
508 + " <error-type>rpc</error-type>"
509 + " <error-tag>missing-attribute</error-tag>"
510 + " <error-severity>error</error-severity>"
511 + " <error-message>Missing attribute</error-message>"
513 + " <bad-attribute>foo</bad-attribute>"
514 + " <bad-element>bar</bad-element>"
519 ByteArrayInputStream bis = new ByteArrayInputStream(xmlStr.getBytes());
520 Document doc = UntrustedXML.newDocumentBuilder().parse(bis);
521 return new NetconfMessage(doc);
524 private static void verifyResponseMessage(final RpcResult<NetconfMessage> rpcResult, final String dataText) {
525 assertNotNull("RpcResult is null", rpcResult);
526 assertTrue("isSuccessful", rpcResult.isSuccessful());
527 NetconfMessage messageResult = rpcResult.getResult();
528 assertNotNull("getResult", messageResult);
529 // List<SimpleNode<?>> nodes = messageResult.getSimpleNodesByName(
530 // QName.create( URI.create( "ns" ), null, "data" ) );
531 // assertNotNull( "getSimpleNodesByName", nodes );
532 // assertEquals( "List<SimpleNode<?>> size", 1, nodes.size() );
533 // assertEquals( "SimpleNode value", dataText, nodes.iterator().next().getValue() );
536 private static RpcError verifyErrorRpcResult(final RpcResult<NetconfMessage> rpcResult,
537 final ErrorType expErrorType, final ErrorTag expErrorTag) {
538 assertNotNull("RpcResult is null", rpcResult);
539 assertFalse("isSuccessful", rpcResult.isSuccessful());
540 assertNotNull("RpcResult errors is null", rpcResult.getErrors());
541 assertEquals("Errors size", 1, rpcResult.getErrors().size());
542 RpcError rpcError = rpcResult.getErrors().iterator().next();
543 assertEquals("getErrorSeverity", ErrorSeverity.ERROR, rpcError.getSeverity());
544 assertEquals("getErrorType", expErrorType, rpcError.getErrorType());
545 assertEquals("getErrorTag", expErrorTag, rpcError.getTag());
547 final String msg = rpcError.getMessage();
548 assertNotNull("getMessage is null", msg);
549 assertFalse("getMessage is empty", msg.isEmpty());
550 assertFalse("getMessage is blank", CharMatcher.whitespace().matchesAllOf(msg));