From ceab07b604f1bf1e0c45fe881d3e6f74b582e8d2 Mon Sep 17 00:00:00 2001 From: Robert Greig Date: Tue, 13 Mar 2007 12:21:36 +0000 Subject: Skeleton of interop testing code added. git-svn-id: https://svn.apache.org/repos/asf/incubator/qpid/trunk/qpid@517664 13f79535-47bb-0310-9956-ffa450edef68 --- .../java/org/apache/qpid/interop/Listener.java | 291 --------------------- .../java/org/apache/qpid/interop/Publisher.java | 244 ----------------- .../interop/coordinator/CoordinatingTestCase.java | 116 ++++++++ .../qpid/interop/coordinator/Coordinator.java | 168 ++++++++++++ .../interop/coordinator/TestClientDetails.java | 35 +++ .../java/org/apache/qpid/interop/old/Listener.java | 291 +++++++++++++++++++++ .../org/apache/qpid/interop/old/Publisher.java | 244 +++++++++++++++++ .../interop/testclient/InteropClientTestCase.java | 95 +++++++ .../apache/qpid/interop/testclient/TestClient.java | 213 +++++++++++++++ .../testclient/testcases/TestCase1DummyRun.java | 75 ++++++ .../testclient/testcases/TestCase2BasicP2P.java | 133 ++++++++++ .../org/apache/qpid/util/ClasspathScanner.java | 146 +++++++++++ .../org/apache/qpid/util/ConversationHelper.java | 167 ++++++++++++ 13 files changed, 1683 insertions(+), 535 deletions(-) delete mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/Listener.java delete mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/Publisher.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/CoordinatingTestCase.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/Coordinator.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/TestClientDetails.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/old/Listener.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/old/Publisher.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/InteropClientTestCase.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/TestClient.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/testcases/TestCase1DummyRun.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/testcases/TestCase2BasicP2P.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/util/ClasspathScanner.java create mode 100644 java/integrationtests/src/main/java/org/apache/qpid/util/ConversationHelper.java (limited to 'java/integrationtests') diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/Listener.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/Listener.java deleted file mode 100644 index dbd07958fd..0000000000 --- a/java/integrationtests/src/main/java/org/apache/qpid/interop/Listener.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * - * 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.qpid.interop; - -import java.util.Random; - -import javax.jms.*; - -import org.apache.log4j.Logger; -import org.apache.log4j.NDC; - -import org.apache.qpid.AMQException; -import org.apache.qpid.client.AMQConnection; -import org.apache.qpid.client.AMQQueue; -import org.apache.qpid.client.AMQSession; -import org.apache.qpid.client.AMQTopic; -import org.apache.qpid.exchange.ExchangeDefaults; -import org.apache.qpid.url.URLSyntaxException; - -/** - * Listener implements the listening end of the Qpid interop tests. It is capable of being run as a standalone listener - * that responds to the test messages send by the publishing end of the tests implemented by {@link Publisher}. - * - *

- *
CRC Card
Responsibilities Collaborations - *
Count messages received on a topic. {@link Publisher} - *
Send reports on messages received, when requested to. {@link Publisher} - *
Shutdown, when requested to. {@link Publisher} - *
- * - * @todo This doesn't implement the interop test spec yet. Its a port of the old topic tests but has been adapted with - * interop spec in mind. - * - * @todo I've added lots of field table types in the report message, just to check if the other end can decode them - * correctly. Not really the right place to test this, so remove them from {@link #sendReport()} once a better - * test exists. - */ -public class Listener implements MessageListener -{ - private static Logger log = Logger.getLogger(Listener.class); - - /** The default AMQ connection URL to use for tests. */ - public static final String DEFAULT_URI = "amqp://guest:guest@default/test?brokerlist='tcp://localhost:5672'"; - - /** Holds the name of (routing key for) the topic to receive test messages on. */ - public static final String CONTROL_TOPIC = "topic_control"; - - /** Holds the name of (routing key for) the queue to send reports to. */ - public static final String RESPONSE_QUEUE = "response"; - - /** Holds the JMS Topic to receive test messages on. */ - private final Topic _topic; - - /** Holds the JMS Queue to send reports to. */ - private final Queue _response; - - /** Holds the connection to listen on. */ - private final Connection _connection; - - /** Holds the producer to send control messages on. */ - private final MessageProducer _controller; - - /** Holds the JMS session. */ - private final javax.jms.Session _session; - - /** Holds a flag to indicate that a timer has begun on the first message. Reset when report is sent. */ - private boolean init; - - /** Holds the count of messages received by this listener. */ - private int count; - - /** Used to hold the start time of the first message. */ - private long start; - - /** - * Creates a topic listener using the specified broker URL. - * - * @param connectionUrl The broker URL to listen on. - * - * @throws AMQException If the broker connection cannot be established. - * @throws URLSyntaxException If the broker URL syntax is not correct. - * @throws JMSException Any underlying JMSException is allowed to fall through. - */ - Listener(String connectionUrl) throws AMQException, JMSException, URLSyntaxException - { - log.debug("Listener(String connectionUrl = " + connectionUrl + "): called"); - - // Create a connection to the broker. - _connection = new AMQConnection(connectionUrl); - - // Establish a session on the broker. - _session = _connection.createSession(false, Session.AUTO_ACKNOWLEDGE); - - // Set up the destinations to listen for test and control messages on. - _topic = _session.createTopic(CONTROL_TOPIC); - _response = _session.createQueue(RESPONSE_QUEUE); - - // Set this listener up to listen for incoming messages on the test topic. - _session.createConsumer(_topic).setMessageListener(this); - - // Set up this listener with a producer to send the reports on. - _controller = _session.createProducer(_response); - - _connection.start(); - System.out.println("Waiting for messages..."); - } - - /** - * Starts a test subscriber. The broker URL must be specified as the first command line argument. - * - * @param argv The command line arguments, ignored. - * - * @todo Add command line arguments to configure all aspects of the test. - */ - public static void main(String[] argv) - { - try - { - new Listener(DEFAULT_URI); - } - catch (Exception e) - { - e.printStackTrace(); - } - } - - /** - * Handles all message received by this listener. Test messages are counted, report messages result in a report being sent and - * shutdown messages result in this listener being terminated. - * - * @param message The received message. - */ - public void onMessage(Message message) - { - log.debug("public void onMessage(Message message = " + message + "): called"); - - // Take the start time of the first message if this is the first message. - if (!init) - { - start = System.nanoTime() / 1000000; - count = 0; - init = true; - } - - try - { - // Check if the message is a control message telling this listener to shut down. - if (isShutdown(message)) - { - log.debug("Got a shutdown message."); - shutdown(); - } - // Check if the message is a report request message asking this listener to respond with the message count. - else if (isReport(message)) - { - log.debug("Got a report request message."); - - // Send the message count report. - sendReport(); - - // Reset the initialization flag so that the next message is considered to be the first. - init = false; - } - // Otherwise it is an ordinary test message, so increment the message count. - else - { - count++; - } - } - catch (JMSException e) - { - log.warn("There was a JMSException during onMessage.", e); - } - } - - /** - * Checks a message to see if it is a termination request control message. - * - * @param m The message to check. - * - * @return true if it is a termination request control message, false otherwise. - * - * @throws JMSException Any underlying JMSException is allowed to fall through. - */ - boolean isShutdown(Message m) throws JMSException - { - boolean result = checkTextField(m, "TYPE", "TERMINATION_REQUEST"); - - return result; - } - - /** - * Checks a message to see if it is a report request control message. - * - * @param m The message to check. - * - * @return true if it is a report request control message, false otherwise. - * - * @throws JMSException Any underlying JMSException is allowed to fall through. - */ - boolean isReport(Message m) throws JMSException - { - boolean result = checkTextField(m, "TYPE", "REPORT_REQUEST"); - - return result; - } - - /** - * Checks whether or not a text field on a message has the specified value. - * - * @param m The message to check. - * @param fieldName The name of the field to check. - * @param value The expected value of the field to compare with. - * - * @return trueIf the specified field has the specified value, fals otherwise. - * - * @throws JMSException Any JMSExceptions are allowed to fall through. - */ - private static boolean checkTextField(Message m, String fieldName, String value) throws JMSException - { - //log.debug("private static boolean checkTextField(Message m = " + m + ", String fieldName = " + fieldName - // + ", String value = " + value + "): called"); - - String comp = m.getStringProperty(fieldName); - //log.debug("comp = " + comp); - - boolean result = (comp != null) && comp.equals(value); - //log.debug("result = " + result); - - return result; - } - - /** - * Closes down the connection to the broker. - * - * @throws JMSException Any underlying JMSException is allowed to fall through. - */ - private void shutdown() throws JMSException - { - _session.close(); - _connection.stop(); - _connection.close(); - } - - /** - * Send the report message to the response queue. - * - * @throws JMSException Any underlying JMSException is allowed to fall through. - */ - private void sendReport() throws JMSException - { - log.debug("private void report(): called"); - - // Create the report message. - long time = ((System.nanoTime() / 1000000) - start); - String msg = "Received " + count + " in " + time + "ms"; - Message message = _session.createTextMessage(msg); - - // Shove some more field table types in the message just to see if the other end can handle it. - message.setBooleanProperty("BOOLEAN", true); - //message.setByteProperty("BYTE", (byte) 5); - message.setDoubleProperty("DOUBLE", Math.PI); - message.setFloatProperty("FLOAT", 1.0f); - message.setIntProperty("INT", 1); - message.setShortProperty("SHORT", (short) 1); - message.setLongProperty("LONG", (long) 1827361278); - message.setStringProperty("STRING", "hello"); - - // Send the report message. - _controller.send(message); - log.debug("Sent report: " + msg); - } -} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/Publisher.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/Publisher.java deleted file mode 100644 index cab679876f..0000000000 --- a/java/integrationtests/src/main/java/org/apache/qpid/interop/Publisher.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * - * 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.qpid.interop; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import javax.jms.*; - -import org.apache.log4j.Logger; - -import org.apache.qpid.AMQException; -import org.apache.qpid.client.AMQConnection; -import org.apache.qpid.url.URLSyntaxException; - -/** - * Publisher is the sending end of Qpid interop tests. It is capable of being run as a standalone publisher - * that sends test messages to the listening end of the tests implemented by {@link Listener}. - * - *

- *
CRC Card
Responsibilities Collaborations - *
- * - * @todo This doesn't implement the interop test spec yet. Its a port of the old topic tests but has been adapted with - * interop spec in mind. - * - * @todo I've added lots of field table types in the report request message, just to check if the other end can decode - * them correctly. Not really the right place to test this, so remove them from {@link #doTest()} once a better - * test exists. - */ -public class Publisher implements MessageListener -{ - private static Logger log = Logger.getLogger(Publisher.class); - - /** The default AMQ connection URL to use for tests. */ - public static final String DEFAULT_URI = "amqp://guest:guest@default/test?brokerlist='tcp://localhost:5672'"; - - /** Holds the default test timeout for broker communications before tests give up. */ - public static final int TIMEOUT = 3000; - - /** Holds the routing key for the topic to send test messages on. */ - public static final String CONTROL_TOPIC = "topic_control"; - - /** Holds the routing key for the queue to receive reports on. */ - public static final String RESPONSE_QUEUE = "response"; - - /** Holds the JMS Topic to send test messages on. */ - private final Topic _topic; - - /** Holds the JMS Queue to receive reports on. */ - private final Queue _response; - - /** Holds the number of messages to send in each test run. */ - private int numMessages; - - /** A monitor used to wait for all reports to arrive back from consumers on. */ - private CountDownLatch allReportsReceivedEvt; - - /** Holds the connection to listen on. */ - private Connection _connection; - - /** Holds the channel for all test messages.*/ - private Session _session; - - /** Holds the producer to send test messages on. */ - private MessageProducer publisher; - - /** - * Creates a topic publisher that will send the specifed number of messages and expect the specifed number of report back from test - * subscribers. - * - * @param connectionUri The broker URL. - * @param numMessages The number of messages to send in each test. - * @param numSubscribers The number of subscribes that are expected to reply with a report. - */ - Publisher(String connectionUri, int numMessages, int numSubscribers) - throws AMQException, JMSException, URLSyntaxException - { - log.debug("Publisher(String connectionUri = " + connectionUri + ", int numMessages = " + numMessages - + ", int numSubscribers = " + numSubscribers + "): called"); - - // Create a connection to the broker. - _connection = new AMQConnection(connectionUri); - - // Establish a session on the broker. - _session = _connection.createSession(false, Session.AUTO_ACKNOWLEDGE); - - // Set up the destinations to send test messages and listen for reports on. - _topic = _session.createTopic(CONTROL_TOPIC); - _response = _session.createQueue(RESPONSE_QUEUE); - - // Set this listener up to listen for reports on the response queue. - _session.createConsumer(_response).setMessageListener(this); - - // Set up this listener with a producer to send the test messages and report requests on. - publisher = _session.createProducer(_topic); - - // Keep the test parameters. - this.numMessages = numMessages; - - // Set up a countdown to count all subscribers sending their reports. - allReportsReceivedEvt = new CountDownLatch(numSubscribers); - - _connection.start(); - System.out.println("Sending messages and waiting for reports..."); - } - - /** - * Start a test publisher. The broker URL must be specified as the first command line argument. - * - * @param argv The command line arguments, ignored. - * - * @todo Add command line arguments to configure all aspects of the test. - */ - public static void main(String[] argv) - { - try - { - // Create an instance of this publisher with the command line parameters. - Publisher publisher = new Publisher(DEFAULT_URI, 1, 1); - - // Publish the test messages. - publisher.doTest(); - } - catch (Exception e) - { - e.printStackTrace(); - } - } - - /** - * Sends the test messages and waits for all subscribers to reply with a report. - * - * @throws JMSException Any underlying JMSException is allowed to fall through. - */ - public void doTest() throws JMSException - { - log.debug("public void DoTest(): called"); - - // Create a test message to send. - Message testMessage = _session.createTextMessage("test"); - - // Send the desired number of test messages. - for (int i = 0; i < numMessages; i++) - { - publisher.send(testMessage); - } - - log.debug("Sent " + numMessages + " test messages."); - - // Send the report request. - Message reportRequestMessage = _session.createTextMessage("Report request message."); - reportRequestMessage.setStringProperty("TYPE", "REPORT_REQUEST"); - - reportRequestMessage.setBooleanProperty("BOOLEAN", false); - //reportRequestMessage.Headers.SetByte("BYTE", 5); - reportRequestMessage.setDoubleProperty("DOUBLE", 3.141); - reportRequestMessage.setFloatProperty("FLOAT", 1.0f); - reportRequestMessage.setIntProperty("INT", 1); - reportRequestMessage.setLongProperty("LONG", 1); - reportRequestMessage.setStringProperty("STRING", "hello"); - reportRequestMessage.setShortProperty("SHORT", (short) 2); - - publisher.send(reportRequestMessage); - - log.debug("Sent the report request message, waiting for all replies..."); - - // Wait until all the reports come in. - try - { - allReportsReceivedEvt.await(TIMEOUT, TimeUnit.MILLISECONDS); - } - catch (InterruptedException e) - { } - - // Check if all reports were really received or if the timeout occurred. - if (allReportsReceivedEvt.getCount() == 0) - { - log.debug("Got all reports."); - } - else - { - log.debug("Waiting for reports timed out, still waiting for " + allReportsReceivedEvt.getCount() + "."); - } - - // Send the termination request. - Message terminationRequestMessage = _session.createTextMessage("Termination request message."); - terminationRequestMessage.setStringProperty("TYPE", "TERMINATION_REQUEST"); - publisher.send(terminationRequestMessage); - - log.debug("Sent the termination request message."); - - // Close all message producers and consumers and the connection to the broker. - shutdown(); - } - - /** - * Handles all report messages from subscribers. This decrements the count of subscribers that are still to reply, until this becomes - * zero, at which time waiting threads are notified of this event. - * - * @param message The received report message. - */ - public void onMessage(Message message) - { - log.debug("public void OnMessage(Message message = " + message + "): called"); - - // Decrement the count of expected messages and release the wait monitor when this becomes zero. - allReportsReceivedEvt.countDown(); - - if (allReportsReceivedEvt.getCount() == 0) - { - log.debug("Got reports from all subscribers."); - } - } - - /** - * Stops the message consumers and closes the connection. - * - * @throws JMSException Any underlying JMSException is allowed to fall through. - */ - private void shutdown() throws JMSException - { - _session.close(); - _connection.close(); - } -} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/CoordinatingTestCase.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/CoordinatingTestCase.java new file mode 100644 index 0000000000..12faa64528 --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/CoordinatingTestCase.java @@ -0,0 +1,116 @@ +/* + * + * 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.qpid.interop.coordinator; + +import java.util.Collection; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.apache.qpid.util.ConversationHelper; + +/** + * An CoordinatingTestCase is a JUnit test case extension that knows how to coordinate test clients that take part in a + * test case as defined in the interop testing specification + * (http://cwiki.apache.org/confluence/display/qpid/Interop+Testing+Specification). + * + *

The real logic of the test cases built on top of this, is embeded in the comparison of the sender and receiver + * reports. An example test method might look like: + * + *

+ * public void testExample()
+ * {
+ *   Properties testConfig = new Properties();
+ *   testConfig.add("TEST_CASE", "example");
+ *   ...
+ *
+ *   Report[] reports = sequenceTest(testConfig);
+ *
+ *   // Compare sender and receiver reports.
+ *   if (report[0] ... report[1] ...)
+ *   {
+ *     Assert.fail("Sender and receiver reports did not match up.");
+ *   }
+ * }
+ *
+ * 
+ * + *

+ *
CRC Card
Responsibilities Collaborations + *
Coordinate the test sequence amongst participants. {@link ConversationHelper} + *
+ */ +public abstract class CoordinatingTestCase extends TestCase +{ + /** + * + * @param sender The contact details of the sending client in the test. + * @param receiver The contact details of the sending client in the test. + * @param allClients The list of all possible test clients that may accept the invitation. + * @param testProperties The test case definition. + */ + public CoordinatingTestCase(TestClientDetails sender, TestClientDetails receiver, + Collection allClients, Properties testProperties) + { } + + /** + * Holds a test coordinating conversation with the test clients. This is the basic implementation of the inner + * loop of Use Case 5. It consists of assign the test roles, begining the test and gathering the test reports + * from the participants. + * + * @param sender The contact details of the sending client in the test. + * @param receiver The contact details of the receiving client in the test. + * @param allParticipatingClients The list of all clients accepted the invitation. + * @param testProperties The test case definition. + * + * @return The test results from the senders and receivers. + */ + protected Object[] sequenceTest(TestClientDetails sender, TestClientDetails receiver, + Collection allParticipatingClients, Properties testProperties) + { + // Check if the sender and recevier did not accept the invite to this test. + { + // Automatically fail this combination of sender and receiver. + } + + // Assign the sender role to the sending test client. + + // Assign the receiver role the receiving client. + + // Wait for the senders and receivers to confirm their roles. + + // Start the test. + + // Wait for the test sender to return its report. + + // As the receiver for its report. + + // Wait for the receiver to send its report. + + return null; + } + + /*protected void setUp() + { }*/ + + /*protected void tearDown() + { }*/ +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/Coordinator.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/Coordinator.java new file mode 100644 index 0000000000..535b4ae014 --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/Coordinator.java @@ -0,0 +1,168 @@ +/* + * + * 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.qpid.interop.coordinator; + +import java.util.Properties; + +import junit.framework.Test; +import junit.framework.TestResult; + +import org.apache.qpid.util.CommandLineParser; + +import uk.co.thebadgerset.junit.extensions.TestRunnerImprovedErrorHandling; + +/** + *

Implements the coordinator client described in the interop testing specification + * (http://cwiki.apache.org/confluence/display/qpid/Interop+Testing+Specification). This coordinator is built on + * top of the JUnit testing framework. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Find out what test clients are available. + *
Decorate available tests to run all available clients. + *
Attach XML test result logger. + *
Terminate the interop testing framework. + *
+ */ +public class Coordinator extends TestRunnerImprovedErrorHandling +{ + /** Holds the URL of the broker to coordinate the tests on. */ + String brokerUrl; + + /** Holds the virtual host to coordinate the tests on. If null, then the default virtual host is used. */ + String virtualHost; + + /** + * Creates an interop test coordinator on the specified broker and virtual host. + * + * @param brokerUrl The URL of the broker to connect to. + * @param virtualHost The virtual host to run all tests on. Optional, may be null. + */ + Coordinator(String brokerUrl, String virtualHost) + { + // Retain the connection parameters. + this.brokerUrl = brokerUrl; + this.virtualHost = virtualHost; + } + + /** + * The entry point for the interop test coordinator. This client accepts the following command line arguments: + * + *

+ *
-b The broker URL. Mandatory. + *
-h The virtual host. Optional. + *
name=value Trailing argument define name/value pairs. Added to system properties. Optional. + *
+ * + * @param args The command line arguments. + */ + public static void main(String[] args) + { + // Use the command line parser to evaluate the command line. + CommandLineParser commandLine = + new CommandLineParser(new String[][] + { + { "b", "The broker URL.", "broker", "true" }, + { "h", "The virtual host to use.", "virtual host", "false" } + }); + + // Capture the command line arguments or display errors and correct usage and then exit. + Properties options = null; + + try + { + options = commandLine.parseCommandLine(args); + } + catch (IllegalArgumentException e) + { + System.out.println(commandLine.getErrors()); + System.out.println(commandLine.getUsage()); + System.exit(1); + } + + // Extract the command line options. + String brokerUrl = options.getProperty("b"); + String virtualHost = options.getProperty("h"); + + // Add all the trailing command line options (name=value pairs) to system properties. Tests may pick up + // overridden values from there. + commandLine.addCommandLineToSysProperties(); + + // Scan for available test cases using a classpath scanner. + String[] testClassNames = null; + + // Create a coordinator and begin its test procedure. + try + { + Coordinator coordinator = new Coordinator(brokerUrl, virtualHost); + TestResult testResult = coordinator.start(testClassNames); + + if (!testResult.wasSuccessful()) + { + System.exit(FAILURE_EXIT); + } + else + { + System.exit(SUCCESS_EXIT); + } + } + catch (Exception e) + { + System.err.println(e.getMessage()); + System.exit(EXCEPTION_EXIT); + } + } + + public TestResult start(String[] testClassNames) throws Exception + { + // Connect to the broker. + + // Broadcast the compulsory invitation to find out what clients are available to test. + + // Wait for a short time, to give test clients an opportunity to reply to the invitation. + + // Retain the list of all available clients. + + // Run all of the tests in the suite using JUnit. + TestResult result = super.start(testClassNames); + + // At this point in time, all tests have completed. Broadcast the shutdown message. + + return result; + } + + /** + * Runs a test or suite of tests, using the super class implemenation. This method wraps the test to be run + * in any test decorators needed to add in the configured toolkits enhanced junit functionality. + * + * @param test The test to run. + * @param wait Undocumented. Nothing in the JUnit javadocs to say what this is for. + * + * @return The results of the test run. + */ + public TestResult doRun(Test test, boolean wait) + { + // Combine together the available test cases and test clients to produce a complete list of test case instances + // to run as a JUnit test suite. + + return null; + } +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/TestClientDetails.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/TestClientDetails.java new file mode 100644 index 0000000000..3a201b6899 --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/coordinator/TestClientDetails.java @@ -0,0 +1,35 @@ +/* + * + * 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.qpid.interop.coordinator; + +/** + *

+ *
CRC Card
Responsibilities Collaborations + *
+ */ +public class TestClientDetails +{ + /** The test clients name. */ + + /* The test clients unqiue sequence number. Not currently used. */ + + /** The routing key of the test clients control topic. */ +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/old/Listener.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/old/Listener.java new file mode 100644 index 0000000000..5545f8d2dc --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/old/Listener.java @@ -0,0 +1,291 @@ +/* + * + * 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.qpid.interop.old; + +import java.util.Random; + +import javax.jms.*; + +import org.apache.log4j.Logger; +import org.apache.log4j.NDC; + +import org.apache.qpid.AMQException; +import org.apache.qpid.client.AMQConnection; +import org.apache.qpid.client.AMQQueue; +import org.apache.qpid.client.AMQSession; +import org.apache.qpid.client.AMQTopic; +import org.apache.qpid.exchange.ExchangeDefaults; +import org.apache.qpid.url.URLSyntaxException; + +/** + * Listener implements the listening end of the Qpid interop tests. It is capable of being run as a standalone listener + * that responds to the test messages send by the publishing end of the tests implemented by {@link org.apache.qpid.interop.old.Publisher}. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Count messages received on a topic. {@link org.apache.qpid.interop.old.Publisher} + *
Send reports on messages received, when requested to. {@link org.apache.qpid.interop.old.Publisher} + *
Shutdown, when requested to. {@link org.apache.qpid.interop.old.Publisher} + *
+ * + * @todo This doesn't implement the interop test spec yet. Its a port of the old topic tests but has been adapted with + * interop spec in mind. + * + * @todo I've added lots of field table types in the report message, just to check if the other end can decode them + * correctly. Not really the right place to test this, so remove them from {@link #sendReport()} once a better + * test exists. + */ +public class Listener implements MessageListener +{ + private static Logger log = Logger.getLogger(Listener.class); + + /** The default AMQ connection URL to use for tests. */ + public static final String DEFAULT_URI = "amqp://guest:guest@default/test?brokerlist='tcp://localhost:5672'"; + + /** Holds the name of (routing key for) the topic to receive test messages on. */ + public static final String CONTROL_TOPIC = "topic_control"; + + /** Holds the name of (routing key for) the queue to send reports to. */ + public static final String RESPONSE_QUEUE = "response"; + + /** Holds the JMS Topic to receive test messages on. */ + private final Topic _topic; + + /** Holds the JMS Queue to send reports to. */ + private final Queue _response; + + /** Holds the connection to listen on. */ + private final Connection _connection; + + /** Holds the producer to send control messages on. */ + private final MessageProducer _controller; + + /** Holds the JMS session. */ + private final javax.jms.Session _session; + + /** Holds a flag to indicate that a timer has begun on the first message. Reset when report is sent. */ + private boolean init; + + /** Holds the count of messages received by this listener. */ + private int count; + + /** Used to hold the start time of the first message. */ + private long start; + + /** + * Creates a topic listener using the specified broker URL. + * + * @param connectionUrl The broker URL to listen on. + * + * @throws AMQException If the broker connection cannot be established. + * @throws URLSyntaxException If the broker URL syntax is not correct. + * @throws JMSException Any underlying JMSException is allowed to fall through. + */ + Listener(String connectionUrl) throws AMQException, JMSException, URLSyntaxException + { + log.debug("Listener(String connectionUrl = " + connectionUrl + "): called"); + + // Create a connection to the broker. + _connection = new AMQConnection(connectionUrl); + + // Establish a session on the broker. + _session = _connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + + // Set up the destinations to listen for test and control messages on. + _topic = _session.createTopic(CONTROL_TOPIC); + _response = _session.createQueue(RESPONSE_QUEUE); + + // Set this listener up to listen for incoming messages on the test topic. + _session.createConsumer(_topic).setMessageListener(this); + + // Set up this listener with a producer to send the reports on. + _controller = _session.createProducer(_response); + + _connection.start(); + System.out.println("Waiting for messages..."); + } + + /** + * Starts a test subscriber. The broker URL must be specified as the first command line argument. + * + * @param argv The command line arguments, ignored. + * + * @todo Add command line arguments to configure all aspects of the test. + */ + public static void main(String[] argv) + { + try + { + new Listener(DEFAULT_URI); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * Handles all message received by this listener. Test messages are counted, report messages result in a report being sent and + * shutdown messages result in this listener being terminated. + * + * @param message The received message. + */ + public void onMessage(Message message) + { + log.debug("public void onMessage(Message message = " + message + "): called"); + + // Take the start time of the first message if this is the first message. + if (!init) + { + start = System.nanoTime() / 1000000; + count = 0; + init = true; + } + + try + { + // Check if the message is a control message telling this listener to shut down. + if (isShutdown(message)) + { + log.debug("Got a shutdown message."); + shutdown(); + } + // Check if the message is a report request message asking this listener to respond with the message count. + else if (isReport(message)) + { + log.debug("Got a report request message."); + + // Send the message count report. + sendReport(); + + // Reset the initialization flag so that the next message is considered to be the first. + init = false; + } + // Otherwise it is an ordinary test message, so increment the message count. + else + { + count++; + } + } + catch (JMSException e) + { + log.warn("There was a JMSException during onMessage.", e); + } + } + + /** + * Checks a message to see if it is a termination request control message. + * + * @param m The message to check. + * + * @return true if it is a termination request control message, false otherwise. + * + * @throws JMSException Any underlying JMSException is allowed to fall through. + */ + boolean isShutdown(Message m) throws JMSException + { + boolean result = checkTextField(m, "TYPE", "TERMINATION_REQUEST"); + + return result; + } + + /** + * Checks a message to see if it is a report request control message. + * + * @param m The message to check. + * + * @return true if it is a report request control message, false otherwise. + * + * @throws JMSException Any underlying JMSException is allowed to fall through. + */ + boolean isReport(Message m) throws JMSException + { + boolean result = checkTextField(m, "TYPE", "REPORT_REQUEST"); + + return result; + } + + /** + * Checks whether or not a text field on a message has the specified value. + * + * @param m The message to check. + * @param fieldName The name of the field to check. + * @param value The expected value of the field to compare with. + * + * @return trueIf the specified field has the specified value, fals otherwise. + * + * @throws JMSException Any JMSExceptions are allowed to fall through. + */ + private static boolean checkTextField(Message m, String fieldName, String value) throws JMSException + { + //log.debug("private static boolean checkTextField(Message m = " + m + ", String fieldName = " + fieldName + // + ", String value = " + value + "): called"); + + String comp = m.getStringProperty(fieldName); + //log.debug("comp = " + comp); + + boolean result = (comp != null) && comp.equals(value); + //log.debug("result = " + result); + + return result; + } + + /** + * Closes down the connection to the broker. + * + * @throws JMSException Any underlying JMSException is allowed to fall through. + */ + private void shutdown() throws JMSException + { + _session.close(); + _connection.stop(); + _connection.close(); + } + + /** + * Send the report message to the response queue. + * + * @throws JMSException Any underlying JMSException is allowed to fall through. + */ + private void sendReport() throws JMSException + { + log.debug("private void report(): called"); + + // Create the report message. + long time = ((System.nanoTime() / 1000000) - start); + String msg = "Received " + count + " in " + time + "ms"; + Message message = _session.createTextMessage(msg); + + // Shove some more field table types in the message just to see if the other end can handle it. + message.setBooleanProperty("BOOLEAN", true); + //message.setByteProperty("BYTE", (byte) 5); + message.setDoubleProperty("DOUBLE", Math.PI); + message.setFloatProperty("FLOAT", 1.0f); + message.setIntProperty("INT", 1); + message.setShortProperty("SHORT", (short) 1); + message.setLongProperty("LONG", (long) 1827361278); + message.setStringProperty("STRING", "hello"); + + // Send the report message. + _controller.send(message); + log.debug("Sent report: " + msg); + } +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/old/Publisher.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/old/Publisher.java new file mode 100644 index 0000000000..f3a545f580 --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/old/Publisher.java @@ -0,0 +1,244 @@ +/* + * + * 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.qpid.interop.old; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.jms.*; + +import org.apache.log4j.Logger; + +import org.apache.qpid.AMQException; +import org.apache.qpid.client.AMQConnection; +import org.apache.qpid.url.URLSyntaxException; + +/** + * Publisher is the sending end of Qpid interop tests. It is capable of being run as a standalone publisher + * that sends test messages to the listening end of the tests implemented by {@link org.apache.qpid.interop.old.Listener}. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
+ * + * @todo This doesn't implement the interop test spec yet. Its a port of the old topic tests but has been adapted with + * interop spec in mind. + * + * @todo I've added lots of field table types in the report request message, just to check if the other end can decode + * them correctly. Not really the right place to test this, so remove them from {@link #doTest()} once a better + * test exists. + */ +public class Publisher implements MessageListener +{ + private static Logger log = Logger.getLogger(Publisher.class); + + /** The default AMQ connection URL to use for tests. */ + public static final String DEFAULT_URI = "amqp://guest:guest@default/test?brokerlist='tcp://localhost:5672'"; + + /** Holds the default test timeout for broker communications before tests give up. */ + public static final int TIMEOUT = 3000; + + /** Holds the routing key for the topic to send test messages on. */ + public static final String CONTROL_TOPIC = "topic_control"; + + /** Holds the routing key for the queue to receive reports on. */ + public static final String RESPONSE_QUEUE = "response"; + + /** Holds the JMS Topic to send test messages on. */ + private final Topic _topic; + + /** Holds the JMS Queue to receive reports on. */ + private final Queue _response; + + /** Holds the number of messages to send in each test run. */ + private int numMessages; + + /** A monitor used to wait for all reports to arrive back from consumers on. */ + private CountDownLatch allReportsReceivedEvt; + + /** Holds the connection to listen on. */ + private Connection _connection; + + /** Holds the channel for all test messages.*/ + private Session _session; + + /** Holds the producer to send test messages on. */ + private MessageProducer publisher; + + /** + * Creates a topic publisher that will send the specifed number of messages and expect the specifed number of report back from test + * subscribers. + * + * @param connectionUri The broker URL. + * @param numMessages The number of messages to send in each test. + * @param numSubscribers The number of subscribes that are expected to reply with a report. + */ + Publisher(String connectionUri, int numMessages, int numSubscribers) + throws AMQException, JMSException, URLSyntaxException + { + log.debug("Publisher(String connectionUri = " + connectionUri + ", int numMessages = " + numMessages + + ", int numSubscribers = " + numSubscribers + "): called"); + + // Create a connection to the broker. + _connection = new AMQConnection(connectionUri); + + // Establish a session on the broker. + _session = _connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + + // Set up the destinations to send test messages and listen for reports on. + _topic = _session.createTopic(CONTROL_TOPIC); + _response = _session.createQueue(RESPONSE_QUEUE); + + // Set this listener up to listen for reports on the response queue. + _session.createConsumer(_response).setMessageListener(this); + + // Set up this listener with a producer to send the test messages and report requests on. + publisher = _session.createProducer(_topic); + + // Keep the test parameters. + this.numMessages = numMessages; + + // Set up a countdown to count all subscribers sending their reports. + allReportsReceivedEvt = new CountDownLatch(numSubscribers); + + _connection.start(); + System.out.println("Sending messages and waiting for reports..."); + } + + /** + * Start a test publisher. The broker URL must be specified as the first command line argument. + * + * @param argv The command line arguments, ignored. + * + * @todo Add command line arguments to configure all aspects of the test. + */ + public static void main(String[] argv) + { + try + { + // Create an instance of this publisher with the command line parameters. + Publisher publisher = new Publisher(DEFAULT_URI, 1, 1); + + // Publish the test messages. + publisher.doTest(); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * Sends the test messages and waits for all subscribers to reply with a report. + * + * @throws JMSException Any underlying JMSException is allowed to fall through. + */ + public void doTest() throws JMSException + { + log.debug("public void DoTest(): called"); + + // Create a test message to send. + Message testMessage = _session.createTextMessage("test"); + + // Send the desired number of test messages. + for (int i = 0; i < numMessages; i++) + { + publisher.send(testMessage); + } + + log.debug("Sent " + numMessages + " test messages."); + + // Send the report request. + Message reportRequestMessage = _session.createTextMessage("Report request message."); + reportRequestMessage.setStringProperty("TYPE", "REPORT_REQUEST"); + + reportRequestMessage.setBooleanProperty("BOOLEAN", false); + //reportRequestMessage.Headers.SetByte("BYTE", 5); + reportRequestMessage.setDoubleProperty("DOUBLE", 3.141); + reportRequestMessage.setFloatProperty("FLOAT", 1.0f); + reportRequestMessage.setIntProperty("INT", 1); + reportRequestMessage.setLongProperty("LONG", 1); + reportRequestMessage.setStringProperty("STRING", "hello"); + reportRequestMessage.setShortProperty("SHORT", (short) 2); + + publisher.send(reportRequestMessage); + + log.debug("Sent the report request message, waiting for all replies..."); + + // Wait until all the reports come in. + try + { + allReportsReceivedEvt.await(TIMEOUT, TimeUnit.MILLISECONDS); + } + catch (InterruptedException e) + { } + + // Check if all reports were really received or if the timeout occurred. + if (allReportsReceivedEvt.getCount() == 0) + { + log.debug("Got all reports."); + } + else + { + log.debug("Waiting for reports timed out, still waiting for " + allReportsReceivedEvt.getCount() + "."); + } + + // Send the termination request. + Message terminationRequestMessage = _session.createTextMessage("Termination request message."); + terminationRequestMessage.setStringProperty("TYPE", "TERMINATION_REQUEST"); + publisher.send(terminationRequestMessage); + + log.debug("Sent the termination request message."); + + // Close all message producers and consumers and the connection to the broker. + shutdown(); + } + + /** + * Handles all report messages from subscribers. This decrements the count of subscribers that are still to reply, until this becomes + * zero, at which time waiting threads are notified of this event. + * + * @param message The received report message. + */ + public void onMessage(Message message) + { + log.debug("public void OnMessage(Message message = " + message + "): called"); + + // Decrement the count of expected messages and release the wait monitor when this becomes zero. + allReportsReceivedEvt.countDown(); + + if (allReportsReceivedEvt.getCount() == 0) + { + log.debug("Got reports from all subscribers."); + } + } + + /** + * Stops the message consumers and closes the connection. + * + * @throws JMSException Any underlying JMSException is allowed to fall through. + */ + private void shutdown() throws JMSException + { + _session.close(); + _connection.close(); + } +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/InteropClientTestCase.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/InteropClientTestCase.java new file mode 100644 index 0000000000..57726285f9 --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/InteropClientTestCase.java @@ -0,0 +1,95 @@ +/* + * + * 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.qpid.interop.testclient; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.Session; + +/** + * InteropClientTestCase provides an interface that classes implementing test cases from the interop testing spec + * (http://cwiki.apache.org/confluence/display/qpid/Interop+Testing+Specification) should implement. Implementations + * must be Java beans, that is, to provide a default constructor and to implement the {@link #getName} method. + * + *

+ *
CRC Card
Responsibilities + *
Supply the name of the test case that this implements. + *
Accept/Reject invites based on test parameters. + *
Adapt to assigned roles. + *
Perform test case actions. + *
Generate test reports. + *
+ */ +public interface InteropClientTestCase extends MessageListener +{ + /** Defines the possible test case roles that an interop test case can take on. */ + public enum Roles + { + SENDER, RECEIVER; + } + + /** + * Should provide the name of the test case that this class implements. The exact names are defined in the + * interop testing spec. + * + * @return The name of the test case that this implements. + */ + public String getName(); + + /** + * Determines whether the test invite that matched this test case is acceptable. + * + * @param inviteMessage The invitation to accept or reject. + * + * @return true to accept the invitation, false to reject it. + * + * @throws JMSException Any JMSException resulting from reading the message are allowed to fall through. + */ + public boolean acceptInvite(Message inviteMessage) throws JMSException; + + /** + * Assigns the role to be played by this test case. The test parameters are fully specified in the + * assignment message. When this method return the test case will be ready to execute. + * + * @param role The role to be played; sender or receiver. + * @param assignRoleMessage The role assingment message, contains the full test parameters. + * + * @throws JMSException Any JMSException resulting from reading the message are allowed to fall through. + */ + public void assignRole(Roles role, Message assignRoleMessage) throws JMSException; + + /** + * Performs the test case actions. + */ + public void start(); + + /** + * Gets a report on the actions performed by the test case in its assigned role. + * + * @param session The session to create the report message in. + * + * @return The report message. + * + * @throws JMSException Any JMSExceptions resulting from creating the report are allowed to fall through. + */ + public Message getReport(Session session) throws JMSException; +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/TestClient.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/TestClient.java new file mode 100644 index 0000000000..2c04a8e52b --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/TestClient.java @@ -0,0 +1,213 @@ +/* + * + * 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.qpid.interop.testclient; + +import java.util.Properties; + +import javax.jms.Message; +import javax.jms.MessageListener; + +import org.apache.qpid.util.CommandLineParser; + +/** + * Implements a test client as described in the interop testing spec + * (http://cwiki.apache.org/confluence/display/qpid/Interop+Testing+Specification). A test client is an agent that + * reacts to control message sequences send by the test {@link org.apache.qpid.interop.coordinator.Coordinator}. + * + *

+ *
Messages Handled by TestClient
Message Action + *
Invite(compulsory) Reply with Enlist. + *
Invite(test case) Reply with Enlist if test case available. + *
AssignRole(test case) Reply with Accept Role if matches an enlisted test. Keep test parameters. + *
Start Send test messages defined by test parameters. Send report on messages sent. + *
Status Request Send report on messages received. + *
+ * + *

+ *
CRC Card
Responsibilities Collaborations + *
Handle all incoming control messages. {@link InteropClientTestCase} + *
Configure and look up test cases by name. {@link InteropClientTestCase} + *
+ */ +public class TestClient implements MessageListener +{ + /** Holds the URL of the broker to run the tests on. */ + String brokerUrl; + + /** Holds the virtual host to run the tests on. If null, then the default virtual host is used. */ + String virtualHost; + + /** Defines an enumeration of the control message types and handling behaviour for each. */ + protected enum ControlMessages implements MessageListener + { + INVITE_COMPULSORY + { + public void onMessage(Message message) + { + // Reply with the client name in an Enlist message. + } + }, + INVITE + { + public void onMessage(Message message) + { + // Extract the test properties. + + // Check if the requested test case is available. + { + // Make the requested test case the current test case. + + // Reply by accepting the invite in an Enlist message. + } + } + }, + ASSIGN_ROLE + { + public void onMessage(Message message) + { + // Extract the test properties. + + // Reply by accepting the role in an Accept Role message. + } + }, + START + { + public void onMessage(Message message) + { + // Start the current test case. + + // Generate the report from the test case and reply with it as a Report message. + } + }, + STATUS_REQUEST + { + public void onMessage(Message message) + { + // Generate the report from the test case and reply with it as a Report message. + } + }, + UNKNOWN + { + public void onMessage(Message message) + { + // Log a warning about this but otherwise ignore it. + } + }; + + /** + * Handles control messages appropriately depending on the message type. + * + * @param message The incoming message to handle. + */ + public abstract void onMessage(Message message); + } + + public TestClient(String brokerUrl, String virtualHost) + { + // Retain the connection parameters. + this.brokerUrl = brokerUrl; + this.virtualHost = virtualHost; + } + + /** + * The entry point for the interop test coordinator. This client accepts the following command line arguments: + * + *

+ *
-b The broker URL. Mandatory. + *
-h The virtual host. Optional. + *
name=value Trailing argument define name/value pairs. Added to system properties. Optional. + *
+ * + * @param args The command line arguments. + */ + public static void main(String[] args) + { + // Use the command line parser to evaluate the command line. + CommandLineParser commandLine = + new CommandLineParser(new String[][] + { + { "b", "The broker URL.", "broker", "true" }, + { "h", "The virtual host to use.", "virtual host", "false" } + }); + + // Capture the command line arguments or display errors and correct usage and then exit. + Properties options = null; + + try + { + options = commandLine.parseCommandLine(args); + } + catch (IllegalArgumentException e) + { + System.out.println(commandLine.getErrors()); + System.out.println(commandLine.getUsage()); + System.exit(1); + } + + // Extract the command line options. + String brokerUrl = options.getProperty("b"); + String virtualHost = options.getProperty("h"); + + // Add all the trailing command line options (name=value pairs) to system properties. Tests may pick up + // overridden values from there. + commandLine.addCommandLineToSysProperties(); + + // Create a test client and start it running. + TestClient client = new TestClient(brokerUrl, virtualHost); + client.start(); + } + + private void start() + { + // Use a class path scanner to find all the interop test case implementations. + + // Create all the test case implementations and index them by the test names. + + // Open a connection to communicate with the coordinator on. + + // Set this up to listen for control messages. + + // Create a producer to send replies with. + } + + /** + * Handles all incoming control messages. + * + * @param message The incoming message. + */ + public void onMessage(Message message) + { + // Delegate the message handling to the message type specific handler. + extractMessageType(message).onMessage(message); + } + + /** + * Determines the control messsage type of incoming messages. + * + * @param message The message to determine the type of. + * + * @return The control message type of the message. + */ + protected ControlMessages extractMessageType(Message message) + { + return null; + } +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/testcases/TestCase1DummyRun.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/testcases/TestCase1DummyRun.java new file mode 100644 index 0000000000..570e4ff25c --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/testcases/TestCase1DummyRun.java @@ -0,0 +1,75 @@ +/* + * + * 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.qpid.interop.testclient.testcases; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; + +import org.apache.qpid.interop.testclient.InteropClientTestCase; + +/** + * Implements tet case 1, dummy run. This test case sends no test messages, it exists to confirm that the test harness + * is interacting with the coordinator correctly. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Supply the name of the test case that this implements. + *
Accept/Reject invites based on test parameters. + *
Adapt to assigned roles. + *
Perform test case actions. + *
Generate test reports. + *
+ */ +public class TestCase1DummyRun implements InteropClientTestCase +{ + public String getName() + { + return "TC1_DummyRun"; + } + + public boolean acceptInvite(Message inviteMessage) throws JMSException + { + // Test parameters don't matter, accept all invites. + return true; + } + + public void assignRole(Roles role, Message assignRoleMessage) throws JMSException + { + // Do nothing, both roles are the same. + } + + public void start() + { + // Do nothing. + } + + public Message getReport(Session session) throws JMSException + { + // Generate a dummy report, the coordinator expects a report but doesn't care what it is. + return session.createTextMessage("Dummy Run, Ok."); + } + + public void onMessage(Message message) + { + // Ignore any messages. + } +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/testcases/TestCase2BasicP2P.java b/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/testcases/TestCase2BasicP2P.java new file mode 100644 index 0000000000..c3c05d8fd9 --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/interop/testclient/testcases/TestCase2BasicP2P.java @@ -0,0 +1,133 @@ +/* + * + * 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.qpid.interop.testclient.testcases; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; + +import org.apache.qpid.interop.testclient.InteropClientTestCase; + +/** + * Implements test case 2, basic P2P. Sends/received a specified number of messages to a specified route on the + * default direct exchange. Produces reports on the actual number of messages sent/received. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Supply the name of the test case that this implements. + *
Accept/Reject invites based on test parameters. + *
Adapt to assigned roles. + *
Send required number of test messages. + *
Generate test reports. + *
+ */ +public class TestCase2BasicP2P implements InteropClientTestCase +{ + /** + * Should provide the name of the test case that this class implements. The exact names are defined in the + * interop testing spec. + * + * @return The name of the test case that this implements. + */ + public String getName() + { + return "TC2_BasicP2P"; + } + + /** + * Determines whether the test invite that matched this test case is acceptable. + * + * @param inviteMessage The invitation to accept or reject. + * + * @return true to accept the invitation, false to reject it. + * + * @throws JMSException Any JMSException resulting from reading the message are allowed to fall through. + */ + public boolean acceptInvite(Message inviteMessage) throws JMSException + { + // All invites are acceptable. + return true; + } + + /** + * Assigns the role to be played by this test case. The test parameters are fully specified in the + * assignment message. When this method return the test case will be ready to execute. + * + * @param role The role to be played; sender or receiver. + * + * @param assignRoleMessage The role assingment message, contains the full test parameters. + * + * @throws JMSException Any JMSException resulting from reading the message are allowed to fall through. + */ + public void assignRole(Roles role, Message assignRoleMessage) throws JMSException + { + // Take note of the role to be played. + + // Extract and retain the test parameters. + + // Create a new connection to pass the test messages on. + + // Check if the sender role is being assigned, and set up a message producer if so. + { + } + // Otherwise the receiver role is being assigned, so set this up to listen for messages. + { + } + } + + /** + * Performs the test case actions. + */ + public void start() + { + // Check that the sender role is being performed. + { + } + } + + /** + * Gets a report on the actions performed by the test case in its assigned role. + * + * @param session The session to create the report message in. + * + * @return The report message. + * + * @throws JMSException Any JMSExceptions resulting from creating the report are allowed to fall through. + */ + public Message getReport(Session session) throws JMSException + { + // Close the test connection. + + // Generate a report message containing the count of the number of messages passed. + + return null; + } + + /** + * Counts incoming test messages. + * + * @param message The incoming test message. + */ + public void onMessage(Message message) + { + // Increment the message count. + } +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/util/ClasspathScanner.java b/java/integrationtests/src/main/java/org/apache/qpid/util/ClasspathScanner.java new file mode 100644 index 0000000000..1dd00da53b --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/util/ClasspathScanner.java @@ -0,0 +1,146 @@ +/* + * + * 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.qpid.util; + +import java.io.File; +import java.util.*; + +/** + * An ClasspathScanner scans the classpath for classes that implement an interface or extend a base class and have names + * that match a regular expression. + * + *

In order to test whether a class implements an interface or extends a class, the class must be loaded (unless + * the class files were to be scanned directly). Using this collector can cause problems when it scans the classpath, + * because loading classes will initialize their statics, which in turn may cause undesired side effects. For this + * reason, the collector should always be used with a regular expression, through which the class file names are + * filtered, and only those that pass this filter will be tested. For example, if you define tests in classes that + * end with the keyword "Test" then use the regular expression "Test$" to match this. + * + *

+ *
CRC Card
Responsibilities Collaborations + *
Find all classes matching type and name pattern on the classpath. + *
+ */ +public class ClasspathScanner +{ + static final int SUFFIX_LENGTH = ".class".length(); + + /** + * Scans the classpath and returns all classes that extend a specified class and match a specified name. + * There is an flag that can be used to indicate that only Java Beans will be matched (that is, only those classes + * that have a default constructor). + * + * @param matchingClass The class or interface to match. + * @param matchingRegexp The reular expression to match against the class name. + * @param beanOnly Flag to indicate that onyl classes with default constructors should be matched. + * + * @return All the classes that match this collector. + */ + public static Collection> getMatches(Class matchingClass, String matchingRegexp, boolean beanOnly) + { + String classPath = System.getProperty("java.class.path"); + Map result = new HashMap(); + + for (String path : splitClassPath(classPath)) + { + gatherFiles(new File(path), "", result); + } + + return result.values(); + } + + private static void gatherFiles(File classRoot, String classFileName, Map result) + { + File thisRoot = new File(classRoot, classFileName); + + if (thisRoot.isFile()) + { + if (matchesName(classFileName)) + { + String className = classNameFromFile(classFileName); + result.put(className, className); + } + + return; + } + + String[] contents = thisRoot.list(); + + if (contents != null) + { + for (String content : contents) + { + gatherFiles(classRoot, classFileName + File.separatorChar + content, result); + } + } + } + + private static boolean matchesName(String classFileName) + { + return classFileName.endsWith(".class") && (classFileName.indexOf('$') < 0) && (classFileName.indexOf("Test") > 0); + } + + private static boolean matchesInterface() + { + return false; + } + + /** + * Takes a classpath (which is a series of paths) and splits it into its component paths. + * + * @param classPath The classpath to split. + * + * @return A list of the component paths that make up the class path. + */ + private static List splitClassPath(String classPath) + { + List result = new LinkedList(); + String separator = System.getProperty("path.separator"); + StringTokenizer tokenizer = new StringTokenizer(classPath, separator); + + while (tokenizer.hasMoreTokens()) + { + result.add(tokenizer.nextToken()); + } + + return result; + } + + /** + * convert /a/b.class to a.b + * + * @param classFileName + * + * @return + */ + private static String classNameFromFile(String classFileName) + { + + String s = classFileName.substring(0, classFileName.length() - SUFFIX_LENGTH); + String s2 = s.replace(File.separatorChar, '.'); + if (s2.startsWith(".")) + { + return s2.substring(1); + } + + return s2; + } +} diff --git a/java/integrationtests/src/main/java/org/apache/qpid/util/ConversationHelper.java b/java/integrationtests/src/main/java/org/apache/qpid/util/ConversationHelper.java new file mode 100644 index 0000000000..bda089045a --- /dev/null +++ b/java/integrationtests/src/main/java/org/apache/qpid/util/ConversationHelper.java @@ -0,0 +1,167 @@ +/* + * + * 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.qpid.util; + +import java.util.Collection; +import java.util.Queue; + +import javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.Message; +import javax.jms.MessageListener; + +/** + * A conversation helper, uses a message correlation id pattern to match up sent and received messages as a conversation + * over JMS messaging. Incoming message traffic is divided up by correlation id. Each id has a queue (behaviour dependant + * on the queue implementation). Clients of this de-multiplexer can wait on messages, defined by message correlation ids. + * The correlating listener is a message listener, and can therefore be attached to a MessageConsumer which is consuming + * from a queue or topic. + * + *

One use of the correlating listener is to act as a conversation synchronizer where multiple threads are carrying + * out conversations over a multiplexed messaging route. This can be usefull, as JMS sessions are not multi-threaded. + * Setting up the correlating listener with synchronous queues will allow these threads to be written in a synchronous + * style, but with their execution order governed by the asynchronous message flow. For example, something like the + * following code could run a multi-threaded conversation (the conversation methods can be called many times in + * parallel): + * + *

+ * MessageListener conversation = new ConversationHelper(java.util.concurrent.LinkedBlockingQueue.class),
+ *                                                       sendDesitination, replyDestination);
+ *
+ * initiateConversation()
+ * {
+ *  try {
+ *   // Exchange greetings.
+ *   conversation.send(conversation.getSession().createTextMessage("Hello."));
+ *   Message greeting = conversation.receive();
+ *
+ *   // Exchange goodbyes.
+ *   conversation.send(conversation.getSession().createTextMessage("Goodbye."));
+ *   Message goodbye = conversation.receive();
+ *  } finally {
+ *   conversation.end();
+ *  }
+ * }
+ *
+ * respondToConversation()
+ * {
+ *   try {
+ *   // Exchange greetings.
+ *   Message greeting = conversation.receive();
+ *   conversation.send(conversation.getSession().createTextMessage("Hello."));
+ *
+ *   // Exchange goodbyes.
+ *   Message goodbye = conversation.receive();
+ *   conversation.send(conversation.getSession().createTextMessage("Goodbye."));
+ *  } finally {
+ *   conversation.end();
+ *  }
+ * }
+ *
+ * 
+ * + *

+ *
CRC Card
Responsibilities Collaborations + *
Associate messages to a conversation using correlation ids. + *
Auto manage sessions for conversations. + *
Store messages not in conversation in dead letter box. + *
+ * + * @todo Non-transactional, can use shared session. Transactional, must have session per-thread. Session pool? In + * transactional mode, commits must happen before receiving, or no replies will come in. (unless there were some + * pending on the queue?). Also, having received on a particular session, must ensure that session is used for all + * subsequent sends and receive at least until the transaction is committed. So a message selector must be used + * to restrict receives on that session to prevent it picking up messages bound for other conversations. + * + * @todo Want something convenient that hides many details. Write out some example use cases to get the best feel for + * it. Pass in connection, send destination, receive destination. Provide endConvo, send, receive + * methods. Bind corrId, session etc. on thread locals. Clean on endConvo. Provide deadLetter box, that + * uncorrelated or late messages go in. Provide time-out on wait methods, and global time-out. + * PingPongProducer provides a good use-case example (sends messages, waits for replies). + * + * @todo New correlationId on every send? or correlation id per conversation? or callers choice. + */ +public class ConversationHelper +{ + /** + * Creates a conversation helper on the specified connection with the default sending destination, and listening + * to the specified receiving destination. + * + * @param connection The connection to build the conversation helper on. + * @param sendDestination The default sending destiation for all messages. + * @param receiveDestination The destination to listen to for incoming messages. + * @param queueClass The queue implementation class. + */ + public ConversationHelper(Connection connection, Destination sendDestination, Destination receiveDestination, + Class queueClass) + { } + + /** + * Sends a message to the default sending location. The correlation id of the message will be assigned by this + * method, overriding any previously set value. + * + * @param message The message to send. + */ + public void send(Message message) + { } + + /** + * Gets the next message in an ongoing conversation. This method may block until such a message is received. + * + * @return The next incoming message in the conversation. + */ + public Message receive() + { + return null; + } + + /** + * Completes the conversation. Any open transactions are committed. Any correlation id's pertaining to the + * conversation are no longer valid, and any incoming messages using them will go to the dead letter box. + */ + public void end() + { } + + /** + * Clears the dead letter box, returning all messages that were in it. + * + * @return All messages in the dead letter box. + */ + public Collection emptyDeadLetterBox() + { + return null; + } + + /** + * Implements the message listener for this conversation handler. + */ + protected class Receiver implements MessageListener + { + /** + * Handles all incoming messages in the ongoing conversations. These messages are split up by correaltion id + * and placed into queues. + * + * @param message The incoming message. + */ + public void onMessage(Message message) + { } + } +} -- cgit v1.2.1