From: Eliezio Oliveira Date: Fri, 28 Jun 2019 08:41:49 +0000 (+0100) Subject: Add E2E testing for testtool X-Git-Tag: release/sodium~30 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=65ab697173701aaff80703e6b8cf5362d8ad74d6;p=netconf.git Add E2E testing for testtool Change-Id: I87d7de23c210f5b4b0480a8737323d3f8ac78eb9 JIRA: NETCONF-630 Signed-off-by: Eliezio Oliveira --- diff --git a/netconf/tools/netconf-testtool/pom.xml b/netconf/tools/netconf-testtool/pom.xml index ce007ea74b..67b11b07b9 100644 --- a/netconf/tools/netconf-testtool/pom.xml +++ b/netconf/tools/netconf-testtool/pom.xml @@ -26,6 +26,7 @@ true + 2.6.3 @@ -134,6 +135,18 @@ org.apache.karaf.features.core ${karaf.version} + + org.xmlunit + xmlunit-core + ${xmlunit.version} + test + + + org.xmlunit + xmlunit-assertj + ${xmlunit.version} + test + @@ -340,6 +353,12 @@ + + maven-surefire-plugin + + plain + + diff --git a/netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java b/netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java index d75cf7b367..7c719cf3d1 100644 --- a/netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java +++ b/netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java @@ -253,7 +253,7 @@ public class NetconfDeviceSimulator implements Closeable { continue; } - LOG.debug("Simulated TCP device started on {}", address); + LOG.debug("Simulated TCP device started on {}", server.channel().localAddress()); } devicesChannels.add(server.channel()); diff --git a/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/LogPropertyCatcher.java b/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/LogPropertyCatcher.java new file mode 100644 index 0000000000..ff934a3d96 --- /dev/null +++ b/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/LogPropertyCatcher.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019 Ericsson Software Technology AB. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.test.tool; + +import static org.slf4j.Logger.ROOT_LOGGER_NAME; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.rules.ExternalResource; +import org.slf4j.LoggerFactory; + +/** + * JUnit Rule that captures a pattern-matching property from log messages. Every time a log messages matches a given + * pattern, the last capturing group will be saved, and can be later retrieved + * via {@link LogPropertyCatcher#getLastValue()}. + * + * @see Rules · junit-team/junit4 Wiki + */ +class LogPropertyCatcher extends ExternalResource { + + private final ListAppender appender; + + LogPropertyCatcher(Pattern pattern) { + this.appender = new ListAppender(pattern); + } + + @Override + protected void before() { + Logger rootLogger = (Logger) LoggerFactory.getLogger(ROOT_LOGGER_NAME); + appender.clear(); + rootLogger.addAppender(appender); + appender.start(); + } + + @Override + protected void after() { + appender.stop(); + Logger rootLogger = (Logger) LoggerFactory.getLogger(ROOT_LOGGER_NAME); + rootLogger.detachAppender(appender); + } + + /** + * Retrieves the last captured property. + * + * @return The last value captured, or Optional.empty() if no log messages matched the pattern. + */ + Optional getLastValue() { + return Optional.ofNullable(appender.lastValue); + } + + private static final class ListAppender extends AppenderBase { + + private final Pattern pattern; + + private String lastValue = null; + + private ListAppender(Pattern pattern) { + this.pattern = pattern; + } + + protected void append(ILoggingEvent evt) { + String msg = evt.getFormattedMessage(); + Matcher matcher = pattern.matcher(msg); + if (matcher.find()) { + lastValue = matcher.group(matcher.groupCount()); + } + } + + void clear() { + this.lastValue = null; + } + } +} diff --git a/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/TestToolTest.java b/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/TestToolTest.java new file mode 100644 index 0000000000..c821ad6990 --- /dev/null +++ b/netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/TestToolTest.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2019 Ericsson Software Technology AB. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.netconf.test.tool; + +import static org.junit.Assert.assertNotNull; +import static org.xmlunit.assertj.XmlAssert.assertThat; + +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.util.HashedWheelTimer; +import io.netty.util.concurrent.DefaultThreadFactory; +import io.netty.util.concurrent.GlobalEventExecutor; +import java.io.File; +import java.net.InetSocketAddress; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.opendaylight.netconf.api.NetconfMessage; +import org.opendaylight.netconf.api.xml.XmlUtil; +import org.opendaylight.netconf.auth.AuthProvider; +import org.opendaylight.netconf.client.NetconfClientDispatcher; +import org.opendaylight.netconf.client.NetconfClientDispatcherImpl; +import org.opendaylight.netconf.client.NetconfClientSession; +import org.opendaylight.netconf.client.NetconfClientSessionListener; +import org.opendaylight.netconf.client.SimpleNetconfClientSessionListener; +import org.opendaylight.netconf.client.conf.NetconfClientConfiguration; +import org.opendaylight.netconf.client.conf.NetconfClientConfiguration.NetconfClientProtocol; +import org.opendaylight.netconf.client.conf.NetconfClientConfigurationBuilder; +import org.opendaylight.netconf.nettyutil.NeverReconnectStrategy; +import org.opendaylight.netconf.nettyutil.handler.ssh.authentication.LoginPasswordHandler; +import org.opendaylight.netconf.test.tool.config.Configuration; +import org.opendaylight.netconf.test.tool.config.ConfigurationBuilder; +import org.w3c.dom.Document; + +@SuppressWarnings("SameParameterValue") +public class TestToolTest { + + private static final long RECEIVE_TIMEOUT_MS = 5_000; + private static final int RANDOM_PORT = 0; + + private static final User ADMIN_USER = new User("admin", "admin"); + private static final File CUSTOM_RPC_CONFIG = new File("src/test/resources/customrpc.xml"); + private static final Configuration SSH_SIMULATOR_CONFIG = getSimulatorConfig(NetconfClientProtocol.SSH, + ADMIN_USER); + private static final Configuration TCP_SIMULATOR_CONFIG = getSimulatorConfig(NetconfClientProtocol.SSH, + ADMIN_USER); + + private static NioEventLoopGroup nettyGroup; + private static NetconfClientDispatcherImpl dispatcher; + + + @Rule + public LogPropertyCatcher logPropertyCatcher = + new LogPropertyCatcher(Pattern.compile("(start\\(\\) listen on auto-allocated port=" + + "|Simulated TCP device started on (/0:0:0:0:0:0:0:0|/0.0.0.0):)(\\d+)")); + + private static final String XML_REQUEST_RFC7950_SECTION_4_2_9 = "\n" + + " \n" + + " example-fw-2.3\n" + + " \n" + + " "; + private static final String EXPECTED_XML_RESPONSE_RFC7950_SECTION_4_2_9 = "\n" + + " \n" + + " The image example-fw-2.3 is being installed.\n" + + " \n" + + " "; + private static final String XML_REQUEST_RFC7950_SECTION_7_15_3 = "\n" + + " \n" + + " \n" + + " apache-1\n" + + " \n" + + " 2014-07-29T13:42:00Z\n" + + " \n" + + " \n" + + " \n" + + " "; + private static final String EXPECTED_XML_RESPONSE_RFC7950_SECTION_7_15_3 = "\n" + + " \n" + + " 2014-07-29T13:42:12Z\n" + + " \n" + + ""; + + @BeforeClass + public static void setUpClass() { + HashedWheelTimer hashedWheelTimer = new HashedWheelTimer(); + nettyGroup = new NioEventLoopGroup(1, new DefaultThreadFactory(NetconfClientDispatcher.class)); + dispatcher = new NetconfClientDispatcherImpl(nettyGroup, nettyGroup, hashedWheelTimer); + } + + @AfterClass + public static void cleanUpClass() + throws InterruptedException { + nettyGroup.shutdownGracefully().sync(); + } + + @Test + public void customRpcOverSsh() + throws Exception { + Document docResponse = invokeRpc(SSH_SIMULATOR_CONFIG, XML_REQUEST_RFC7950_SECTION_7_15_3); + assertThat(docResponse) + .and(EXPECTED_XML_RESPONSE_RFC7950_SECTION_7_15_3) + .ignoreWhitespace() + .areIdentical(); + } + + @Test + public void customRpcOverTcp() + throws Exception { + Document docResponse = invokeRpc(TCP_SIMULATOR_CONFIG, XML_REQUEST_RFC7950_SECTION_4_2_9); + assertThat(docResponse) + .and(EXPECTED_XML_RESPONSE_RFC7950_SECTION_4_2_9) + .ignoreWhitespace() + .areIdentical(); + } + + private Document invokeRpc(Configuration simulatorConfig, String xmlRequest) + throws Exception { + // GIVEN + int localPort = launchSimulator(simulatorConfig); + SimpleNetconfClientSessionListener sessionListener = new SimpleNetconfClientSessionListener(); + NetconfClientConfiguration clientConfig = getClientConfig("localhost", localPort, + simulatorConfig, sessionListener); + Document docRequest = XmlUtil.readXmlToDocument(xmlRequest); + NetconfMessage request = new NetconfMessage(docRequest); + + // WHEN + NetconfMessage response; + try (NetconfClientSession ignored = dispatcher.createClient(clientConfig).get()) { + response = sessionListener.sendRequest(request) + .get(RECEIVE_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + + // THEN + assertNotNull(response); + return response.getDocument(); + } + + private static final ConcurrentHashMap CACHED_SIMULATORS = new ConcurrentHashMap<>(); + + /** + * Retrieves a previously launched simulator or launches a new one using the given configuration. + * + * @param configuration The simulator configuration. + * @return The TCP port number to access the launched simulator. + */ + private int launchSimulator(Configuration configuration) { + return CACHED_SIMULATORS.computeIfAbsent(configuration, cfg -> { + NetconfDeviceSimulator simulator = new NetconfDeviceSimulator(cfg); + simulator.start(); + return logPropertyCatcher.getLastValue() + .map(Integer::parseInt) + .orElseThrow(() -> new IllegalArgumentException("Unable to capture auto-allocated port from log")); + }); + } + + @SuppressWarnings("deprecation") + private static Configuration getSimulatorConfig(NetconfClientProtocol protocol, User user) { + return new ConfigurationBuilder() + .setStartingPort(RANDOM_PORT) + .setRpcConfigFile(CUSTOM_RPC_CONFIG) + .setSsh(protocol == NetconfClientProtocol.SSH) + .setAuthProvider(new InMemoryAuthenticationProvider(user)) + .build(); + } + + @SuppressWarnings("deprecation") + private static NetconfClientConfiguration getClientConfig(String host, int port, + Configuration simulatorConfig, + NetconfClientSessionListener sessionListener) { + User user = ((InMemoryAuthenticationProvider) simulatorConfig.getAuthProvider()).user; + return NetconfClientConfigurationBuilder.create() + .withAddress(new InetSocketAddress(host, port)) + .withSessionListener(sessionListener) + .withReconnectStrategy(new NeverReconnectStrategy(GlobalEventExecutor.INSTANCE, + NetconfClientConfigurationBuilder.DEFAULT_CONNECTION_TIMEOUT_MILLIS)) + .withProtocol(simulatorConfig.isSsh() ? NetconfClientProtocol.SSH : NetconfClientProtocol.TCP) + .withAuthHandler(new LoginPasswordHandler(user.username, user.password)) + .build(); + } + + private static final class User { + private final String username; + private final String password; + + private User(String username, String password) { + this.username = username; + this.password = password; + } + } + + private static final class InMemoryAuthenticationProvider implements AuthProvider { + + private final User user; + + private InMemoryAuthenticationProvider(User user) { + this.user = user; + } + + @Override + public boolean authenticated(String username, String password) { + return user.username.equals(username) && user.password.equals(password); + } + } +} diff --git a/netconf/tools/netconf-testtool/src/test/resources/customrpc.xml b/netconf/tools/netconf-testtool/src/test/resources/customrpc.xml new file mode 100644 index 0000000000..754bdb0500 --- /dev/null +++ b/netconf/tools/netconf-testtool/src/test/resources/customrpc.xml @@ -0,0 +1,88 @@ + + + + + + + example-fw-2.3 + + + + + + The image example-fw-2.3 is being installed. + + + + + + + + + + + eth1 + + 192.0.2.1 + + + + + + + 60 + + + + + + + + + 27606-0100 + + + + + + + + + + + + + + + apache-1 + + 2014-07-29T13:42:00Z + + + + + + + + 2014-07-29T13:42:12Z + + + + + + + + + Foo-One + 2 + + + + + + + + + +