/*
* Copyright 2015 the original author or authors.
*
* Licensed 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.springframework.cloud.consul.binder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.consul.binder.test.consumer.TestConsumer;
import org.springframework.cloud.consul.binder.test.producer.TestProducer;
import org.springframework.cloud.deployer.spi.app.AppDeployer;
import org.springframework.cloud.deployer.spi.core.AppDefinition;
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
import org.springframework.cloud.deployer.spi.local.LocalAppDeployer;
import org.springframework.cloud.deployer.spi.local.LocalDeployerProperties;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.util.SocketUtils;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
/**
* Tests for {@link org.springframework.cloud.consul.binder.ConsulBinder}.
*
* @author Spencer Gibb
*/
public class ConsulBinderTests {
private static final Logger logger = LoggerFactory.getLogger(ConsulBinderTests.class);
/**
* Timeout value in milliseconds for operations to complete.
*/
private static final long TIMEOUT = 30000;
/**
* Payload of test message.
*/
public static final String MESSAGE_PAYLOAD = "hello world";
/**
* Name of binding used for producer and consumer bindings.
*/
public static final String BINDING_NAME = "test";
/**
* Deployer to launch producer and consumer test applications.
*/
private final AppDeployer deployer;
/**
* Rest template for communicating with producer/consumer test applications.
*/
private final RestTemplate restTemplate = new RestTemplate();
public ConsulBinderTests() {
LocalDeployerProperties properties = new LocalDeployerProperties();
properties.setDeleteFilesOnExit(false);
this.deployer = new ClasspathDeployer(properties);
}
/**
* Test basic message sending functionality.
*
* @throws Exception
*/
@Test
public void testMessageSendReceive() throws Exception {
testMessageSendReceive(null);
}
/**
* Test usage of partition selector.
*
* @throws Exception
*/
/*@Test
public void testPartitionedMessageSendReceive() throws Exception {
testMessageSendReceive(null, true);
}*/
/**
* Test consumer group functionality.
*
* @throws Exception
*/
/*@Test
public void testMessageSendReceiveConsumerGroups() throws Exception {
testMessageSendReceive(new String[]{"a", "b"}, false);
}*/
/**
* Test message sending functionality.
*
* @param groups consumer groups; may be {@code null}
* @param partitioned if true, execute test with a partition selector
* @throws Exception
*/
private void testMessageSendReceive(String[] groups) throws Exception {
Set<AppId> consumers = null;
AppId producer = null;
try {
consumers = launchConsumers(groups);
producer = launchProducer();
for (AppId consumer : consumers) {
assertEquals(MESSAGE_PAYLOAD, waitForMessage(consumer.port));
}
}
finally {
if (producer != null) {
shutdownApplication(producer.id);
}
if (consumers != null) {
for (AppId consumer : consumers) {
shutdownApplication(consumer.id);
}
}
}
}
/**
* Launch one or more consumers based on the number of consumer groups.
* Blocks execution until the consumers are bound.
*
* @param groups consumer groups; may be {@code null}
* @return a set of {@link AppId}s for the consumers
* @throws InterruptedException
*/
private Set<AppId> launchConsumers(String[] groups) throws InterruptedException {
Set<AppId> consumers = new HashSet<>();
Map<String, String> appProperties = new HashMap<>();
int consumerCount = groups == null ? 1 : groups.length;
for (int i = 0; i < consumerCount; i++) {
int consumerPort = SocketUtils.findAvailableTcpPort();
appProperties.put("server.port", String.valueOf(consumerPort));
List<String> args = new ArrayList<>();
args.add(String.format("--server.port=%d", consumerPort));
args.add("--debug");
if (groups != null) {
args.add(String.format("--group=%s", groups[i]));
}
consumers.add(new AppId(launchApplication(TestConsumer.class, appProperties, args), consumerPort));
}
for (AppId app : consumers) {
waitForConsumer(app.port);
}
return consumers;
}
/**
* Launch a producer that publishes a test message.
*
* @return {@link AppId} for producer
*/
private AppId launchProducer() {
int producerPort = SocketUtils.findAvailableTcpPort();
Map<String, String> appProperties = new HashMap<>();
appProperties.put("server.port", String.valueOf(producerPort));
List<String> args = new ArrayList<>();
args.add(String.format("--server.port=%d", producerPort));
args.add(String.format("--partitioned=%b", false));
args.add("--debug");
return new AppId(launchApplication(TestProducer.class, appProperties, args), producerPort);
}
/**
* Block the executing thread until the consumer is bound.
*
* @param port server port of the consumer application
* @throws InterruptedException if the thread is interrupted
* @throws AssertionError if the consumer is not bound after
* {@value #TIMEOUT} milliseconds
*/
private void waitForConsumer(int port) throws InterruptedException {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() < start + TIMEOUT) {
if (isConsumerBound(port)) {
return;
}
else {
Thread.sleep(1000);
}
}
assertTrue("Consumer not bound", isConsumerBound(port));
}
/**
* Return {@code true} if the consumer at the provided port is bound.
*
* @param port http port for consumer
* @return true if consumer is bound
*/
private boolean isConsumerBound(int port) {
try {
return restTemplate.getForObject(
String.format("http://localhost:%d/is-bound", port), Boolean.class);
}
catch (ResourceAccessException e) {
logger.trace("isConsumerBound", e);
return false;
}
}
/**
* Return the most recent payload message a consumer received.
*
* @param port http port for consumer
* @return the most recent payload message a consumer received;
* may be {@code null}
*/
private String getConsumerMessagePayload(int port) {
try {
return restTemplate.getForObject(
String.format("http://localhost:%d/message-payload", port), String.class);
}
catch (ResourceAccessException e) {
logger.debug("getConsumerMessagePayload", e);
return null;
}
}
/**
* Return {@code true} if the producer made use of a custom partition selector.
*
* @param port http port for producer
* @return true if the producer used a custom partition selector
*/
private boolean partitionSelectorUsed(int port) throws InterruptedException {
try {
return restTemplate.getForObject(
String.format("http://localhost:%d/partition-strategy-invoked", port),
Boolean.class);
}
catch (ResourceAccessException e) {
logger.debug("partitionSelectorUsed", e);
return false;
}
}
/**
* Block the executing thread until a message is received by the
* consumer application, or until {@value #TIMEOUT} milliseconds elapses.
*
* @param port server port of the consumer application
* @return the message payload that was received
* @throws InterruptedException if the thread is interrupted
*/
private String waitForMessage(int port) throws InterruptedException {
long start = System.currentTimeMillis();
String message = null;
while (System.currentTimeMillis() < start + TIMEOUT) {
message = getConsumerMessagePayload(port);
if (message == null) {
Thread.sleep(1000);
}
else {
break;
}
}
return message;
}
/**
* Launch an application in a separate JVM.
*
* @param clz the main class to launch
* @param properties the properties to pass to the application
* @param args the command line arguments for the application
* @return a string identifier for the application
*/
private String launchApplication(Class<?> clz, Map<String, String> properties, List<String> args) {
Resource resource = new UrlResource(clz.getProtectionDomain().getCodeSource().getLocation());
properties.put(AppDeployer.GROUP_PROPERTY_KEY, "test-group");
properties.put("main", clz.getName());
properties.put("classpath", System.getProperty("java.class.path"));
String appName = String.format("%s-%s", clz.getSimpleName(), properties.get("server.port"));
AppDefinition definition = new AppDefinition(appName, properties);
AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, properties, args);
return this.deployer.deploy(request);
}
/**
* Shut down the application with the provided id.
*
* @param id id of application to shut down
*/
private void shutdownApplication(String id) {
this.deployer.undeploy(id);
}
private static class ClasspathDeployer extends LocalAppDeployer {
/**
* Instantiates a new local app deployer.
*
* @param properties the properties
*/
ClasspathDeployer(LocalDeployerProperties properties) {
super(properties);
}
/**
* Builds the jar execution command.
*
* @param jarPath the jar path
* @param request the request
* @return the string[]
*/
protected String[] buildJarExecutionCommand(String jarPath, AppDeploymentRequest request) {
ArrayList<String> commands = new ArrayList<>();
commands.add(super.getLocalDeployerProperties().getJavaCmd());
commands.add("-cp");
commands.add(request.getDefinition().getProperties().get("classpath"));
commands.add(request.getDefinition().getProperties().get("main"));
commands.addAll(request.getCommandlineArguments());
return commands.toArray(new String[commands.size()]);
}
}
/**
* String identification and http port for a launched application.
*/
private static class AppId {
final String id;
final int port;
AppId(String id, int port) {
this.id = id;
this.port = port;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AppId appId = (AppId) o;
return port == appId.port && id.equals(appId.id);
}
@Override
public int hashCode() {
int result = id.hashCode();
result = 31 * result + port;
return result;
}
}
}