Add E2E testing for testtool 12/82912/2
authorEliezio Oliveira <eliezio.oliveira@est.tech>
Fri, 28 Jun 2019 08:41:49 +0000 (09:41 +0100)
committerEliezio Oliveira <eliezio.oliveira@est.tech>
Thu, 4 Jul 2019 11:20:35 +0000 (12:20 +0100)
Change-Id: I87d7de23c210f5b4b0480a8737323d3f8ac78eb9
JIRA: NETCONF-630
Signed-off-by: Eliezio Oliveira <eliezio.oliveira@est.tech>
netconf/tools/netconf-testtool/pom.xml
netconf/tools/netconf-testtool/src/main/java/org/opendaylight/netconf/test/tool/NetconfDeviceSimulator.java
netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/LogPropertyCatcher.java [new file with mode: 0644]
netconf/tools/netconf-testtool/src/test/java/org/opendaylight/netconf/test/tool/TestToolTest.java [new file with mode: 0644]
netconf/tools/netconf-testtool/src/test/resources/customrpc.xml [new file with mode: 0644]

index ce007ea74b5be63f607fe493ac65817f1732fe14..67b11b07b9f6b74c133dbb5c94128c5c243e21eb 100644 (file)
@@ -26,6 +26,7 @@
 
     <properties>
         <sonar.skip>true</sonar.skip>
+        <xmlunit.version>2.6.3</xmlunit.version>
     </properties>
 
     <dependencies>
             <artifactId>org.apache.karaf.features.core</artifactId>
             <version>${karaf.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.xmlunit</groupId>
+            <artifactId>xmlunit-core</artifactId>
+            <version>${xmlunit.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.xmlunit</groupId>
+            <artifactId>xmlunit-assertj</artifactId>
+            <version>${xmlunit.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
                       </execution>
                   </executions>
               </plugin>
+              <plugin>
+                  <artifactId>maven-surefire-plugin</artifactId>
+                  <configuration>
+                      <reportFormat>plain</reportFormat>
+                  </configuration>
+              </plugin>
           </plugins>
       </build>
 
index d75cf7b367766bee0817a23652e9ced9f8a172f1..7c719cf3d16c7fd76ff7e3fbbd3113c2299e01d9 100644 (file)
@@ -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 (file)
index 0000000..ff934a3
--- /dev/null
@@ -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 <a href="https://github.com/junit-team/junit4/wiki/Rules">Rules ยท junit-team/junit4 Wiki</a>
+ */
+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<String> getLastValue() {
+        return Optional.ofNullable(appender.lastValue);
+    }
+
+    private static final class ListAppender extends AppenderBase<ILoggingEvent> {
+
+        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 (file)
index 0000000..c821ad6
--- /dev/null
@@ -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 = "<rpc message-id=\"101\"\n"
+        + "          xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+        + "       <activate-software-image xmlns=\"http://example.com/system\">\n"
+        + "         <image-name>example-fw-2.3</image-name>\n"
+        + "       </activate-software-image>\n"
+        + "     </rpc>";
+    private static final String EXPECTED_XML_RESPONSE_RFC7950_SECTION_4_2_9 = "<rpc-reply message-id=\"101\"\n"
+        + "                xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+        + "       <status xmlns=\"http://example.com/system\">\n"
+        + "         The image example-fw-2.3 is being installed.\n"
+        + "       </status>\n"
+        + "     </rpc-reply>";
+    private static final String XML_REQUEST_RFC7950_SECTION_7_15_3 = "<rpc message-id=\"101\"\n"
+        + "          xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+        + "       <action xmlns=\"urn:ietf:params:xml:ns:yang:1\">\n"
+        + "         <server xmlns=\"urn:example:server-farm\">\n"
+        + "           <name>apache-1</name>\n"
+        + "           <reset>\n"
+        + "             <reset-at>2014-07-29T13:42:00Z</reset-at>\n"
+        + "           </reset>\n"
+        + "         </server>\n"
+        + "       </action>\n"
+        + "     </rpc>";
+    private static final String EXPECTED_XML_RESPONSE_RFC7950_SECTION_7_15_3 = "<rpc-reply message-id=\"101\"\n"
+        + "           xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n"
+        + "  <reset-finished-at xmlns=\"urn:example:server-farm\">\n"
+        + "    2014-07-29T13:42:12Z\n"
+        + "  </reset-finished-at>\n"
+        + "</rpc-reply>";
+
+    @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<Configuration, Integer> 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 (file)
index 0000000..754bdb0
--- /dev/null
@@ -0,0 +1,88 @@
+<rpcs>
+
+  <!-- First example from https://tools.ietf.org/html/rfc7950#section-4.2.9 -->
+  <rpc>
+    <input>
+      <activate-software-image xmlns="http://example.com/system">
+        <image-name>example-fw-2.3</image-name>
+      </activate-software-image>
+    </input>
+    <output>
+      <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+        <status xmlns="http://example.com/system">
+          The image example-fw-2.3 is being installed.
+        </status>
+      </rpc-reply>
+    </output>
+  </rpc>
+
+  <!-- Second example from https://tools.ietf.org/html/rfc7950#section-4.2.9 -->
+  <rpc>
+    <input>
+      <action xmlns="urn:ietf:params:xml:ns:yang:1">
+        <interface xmlns="http://example.com/system">
+          <name>eth1</name>
+          <ping>
+            <destination>192.0.2.1</destination>
+          </ping>
+        </interface>
+      </action>
+    </input>
+    <output>
+      <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"
+                 xmlns:sys="http://example.com/system">
+        <sys:packet-loss>60</sys:packet-loss>
+      </rpc-reply>
+    </output>
+  </rpc>
+
+  <!-- Example from https://tools.ietf.org/html/rfc7950#section-7.14.5 -->
+  <rpc>
+    <input>
+      <rock-the-house xmlns="urn:example:rock">
+        <zip-code>27606-0100</zip-code>
+      </rock-the-house>
+    </input>
+    <output>
+      <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+        <ok/>
+      </rpc-reply>
+    </output>
+  </rpc>
+
+  <!-- Example from https://tools.ietf.org/html/rfc7950#section-7.15.3 -->
+  <rpc>
+    <input>
+      <action xmlns="urn:ietf:params:xml:ns:yang:1">
+        <server xmlns="urn:example:server-farm">
+          <name>apache-1</name>
+          <reset>
+            <reset-at>2014-07-29T13:42:00Z</reset-at>
+          </reset>
+        </server>
+      </action>
+    </input>
+    <output>
+      <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+        <reset-finished-at xmlns="urn:example:server-farm">
+          2014-07-29T13:42:12Z
+        </reset-finished-at>
+      </rpc-reply>
+    </output>
+  </rpc>
+
+  <rpc>
+    <input>
+      <play xmlns="urn:mynetconf:test">
+        <playlist>Foo-One</playlist>
+        <song-number>2</song-number>
+      </play>
+    </input>
+    <output>
+      <rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
+        <ok/>
+      </rpc-reply>
+    </output>
+  </rpc>
+
+</rpcs>