/**************************************************************************************
* Copyright (C) 2009 Progress Software, Inc. All rights reserved. *
* http://fusesource.com *
* ---------------------------------------------------------------------------------- *
* The software in this package is published under the terms of the AGPL license *
* a copy of which has been included with this distribution in the license.txt file. *
**************************************************************************************/
package org.fusesource.cloudmix.testing;
import com.sun.jersey.api.client.filter.LoggingFilter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.fusesource.cloudmix.agent.RestGridClient;
import org.fusesource.cloudmix.agent.logging.LogRecord;
import org.fusesource.cloudmix.common.CloudmixHelper;
import org.fusesource.cloudmix.common.GridClient;
import org.fusesource.cloudmix.common.GridClients;
import org.fusesource.cloudmix.common.ProcessClient;
import org.fusesource.cloudmix.common.dto.AgentDetails;
import org.fusesource.cloudmix.common.dto.Dependency;
import org.fusesource.cloudmix.common.dto.DependencyStatus;
import org.fusesource.cloudmix.common.dto.FeatureDetails;
import org.fusesource.cloudmix.common.dto.ProfileDetails;
import org.fusesource.cloudmix.common.dto.ProfileStatus;
import org.junit.After;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.rules.TestName;
import javax.ws.rs.core.UriBuilder;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Creates a new temporary environment for a distributed test,
* initialises the system, then runs the test and kills the enviroment
*
* @version $Revision: 1.1 $
*/
public abstract class TestController {
/**
* The name of the file which all the newly created profile IDs are written on each test run.
* You can then clean up your test cloud by deleting all of the profiles in this file
*/
public static final String PROFILE_ID_FILENAME = "cloudmix.profiles";
private static final transient Log LOG = LogFactory.getLog(TestController.class);
//CHECKSTYLE:OFF
@Rule
public TestName testName = new TestName();
//CHECKSTYLE:ON
protected long startupTimeout = 60 * 1000;
protected String controllerUrl = CloudmixHelper.getDefaultRootUrl();
protected List<FeatureDetails> features = new CopyOnWriteArrayList<FeatureDetails>();
protected RestGridClient gridClient;
protected ProfileDetails profile;
protected String profileId;
protected boolean provisioned;
protected boolean destroyProfileAfter;
protected boolean destroyOtherProfilesOnStartup = true;
protected boolean logRestOperations;
protected String getTestName() {
String answer = testName.getMethodName();
if (answer == null || answer.length() == 0) {
return "Unknown";
}
return answer;
}
/**
* Registers any features which are required for this system test
*/
protected abstract void installFeatures();
/**
* Factory method to create a new feature which is unique to the current test's profile
*/
protected FeatureDetails createFeatureDetails(String featureId, String uri) {
FeatureDetails answer = new FeatureDetails(featureId, uri);
ensureFeatureIdLocalToProfile(answer);
return answer;
}
/**
* Asserts that the test cloud is setup and provisioned properly within the given {@link #startupTimeout}.
* <p/>
* This method should be called within each test method so that the profile is setup
* correctly with the class of the test and the test method name.
*/
public void checkProvisioned() throws Exception {
try {
if (provisioned) {
return;
}
// lets get the default URL for cloudmix
System.out.println("Using controller URL: " + controllerUrl);
// lets register the features
GridClient controller = getGridClient();
// allow system property to override this value
String systemProperty = "cloudmix.destroyOtherProfilesOnStartup";
String flag = System.getProperty(systemProperty);
if (flag != null) {
try {
destroyOtherProfilesOnStartup = Boolean.parseBoolean(flag);
} catch (Exception e) {
LOG.error("Failed to parse boolean system property " + systemProperty + " with value: " + flag + ". Reason: " + e, e);
}
}
else if (destroyOtherProfilesOnStartup) {
LOG.info("About to destroy all previous JUnit profiles on the CloudMix server. " +
"To disable this behaviour set the destroyOtherProfilesOnStartup field to false on your JUnit class or set the '" +
systemProperty + "' system property to 'false''");
}
if (destroyOtherProfilesOnStartup) {
destroyCurrentProfiles();
}
if (profileId == null) {
profileId = UUID.randomUUID().toString();
}
// lets append the profileId to the file!
onProfileIdCreated(profileId);
profile = new ProfileDetails(profileId);
installFeatures();
for (FeatureDetails feature : features) {
ensureFeatureIdLocalToProfile(feature);
profile.getFeatures().add(new Dependency(feature.getId()));
System.out.println("Adding feature: " + feature.getId());
controller.addFeature(feature);
}
profile.setDescription(createProfileDescription(profile));
controller.addProfile(profile);
// now lets start the remote grid
assertProvisioned();
provisioned = true;
System.out.println("All features provisioned!!");
} catch (Exception e) {
System.out.println("Caught: " + e);
e.printStackTrace();
Throwable t = e;
while (true) {
Throwable throwable = t.getCause();
if (throwable == t || throwable == null) {
break;
}
System.out.println("Caused by : " + throwable);
throwable.printStackTrace();
t = throwable;
}
LOG.error("Caught: " + e, e);
throw e;
}
}
/**
* Destroys
*/
protected void destroyCurrentProfiles() {
File file = new File(PROFILE_ID_FILENAME);
if (file.exists()) {
try {
BufferedReader reader = new BufferedReader(new FileReader(file));
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
line = line.trim();
if (line.startsWith("#") || line.length() == 0) {
continue;
}
LOG.info("Destroying old profile: " + line);
gridClient.removeFeature(line);
}
file.delete();
} catch (IOException e) {
LOG.error("Failed to read old profiles to file: " + file);
}
}
}
protected List<? extends ProcessClient> getProcessClientsFor(FeatureDetails featureDetails)
throws URISyntaxException {
return getProcessClientsFor(id(featureDetails));
}
private List<? extends ProcessClient> getProcessClientsFor(String featureId) throws URISyntaxException {
return getGridClient().getProcessClientsForFeature(featureId);
}
/**
* Returns all the agents which are running the given feature
*/
protected List<AgentDetails> getAgentsFor(FeatureDetails featureDetails) throws URISyntaxException {
return getAgentsFor(id(featureDetails));
}
protected String id(FeatureDetails featureDetails) {
String id = featureDetails.getId();
Assert.assertNotNull("Feature should have an ID " + featureDetails, id);
return id;
}
/**
* Returns all the agents which are running the given feature ID
*/
protected List<AgentDetails> getAgentsFor(String featureId) throws URISyntaxException {
return GridClients.getAgentDetailsAssignedToFeature(getGridClient(), featureId);
}
protected String createProfileDescription(ProfileDetails pd) {
return "CloudMix test case for class <b>" + getClass().getName()
+ "</b> with test method <b>" + getTestName() + "</b>";
}
/**
* associate the feature with the profile, so that when the profile is deleted, so is the feature
*/
protected void ensureFeatureIdLocalToProfile(FeatureDetails feature) {
Assert.assertNotNull("profile ID should be defined!", profileId);
// lets ensure the feature ID is unique (though the code could be smart enough to deduce it!)
String featureId = feature.getId();
if (!featureId.startsWith(profileId)) {
featureId = profileId + ":" + featureId;
feature.setId(featureId);
}
feature.setOwnedByProfileId(profileId);
}
protected void onProfileIdCreated(String profileid) throws IOException {
String fileName = PROFILE_ID_FILENAME;
try {
FileWriter writer = new FileWriter(fileName, true);
writer.append(profileid);
writer.append("\n");
writer.close();
} catch (IOException e) {
LOG.error("Failed to write profileId to file: " + fileName);
}
}
@After
public void tearDown() throws Exception {
if (destroyProfileAfter) {
if (gridClient != null) {
if (profile != null) {
gridClient.removeProfile(profile);
}
}
provisioned = false;
}
}
public RestGridClient getGridClient() throws URISyntaxException {
if (gridClient == null) {
gridClient = createGridController();
}
return gridClient;
}
public void setGridClient(RestGridClient gridClient) {
this.gridClient = gridClient;
}
/**
* Returns a newly created client. Factory method
*/
protected RestGridClient createGridController() throws URISyntaxException {
System.out.println("About to create RestGridClient for: " + controllerUrl);
RestGridClient answer = new RestGridClient(controllerUrl);
if (logRestOperations) {
answer.getClient(null).addFilter(new LoggingFilter());
}
return answer;
}
/**
* Allow a feature to be registered prior to starting the profile
*/
protected void addFeature(FeatureDetails featureDetails) {
features.add(featureDetails);
}
/**
* Allows feature to be registered prior to starting the profile
*/
protected void addFeatures(FeatureDetails... featureDetails) {
for (FeatureDetails featureDetail : featureDetails) {
addFeature(featureDetail);
}
}
/**
* Allows feature to be registered prior to starting the profile
*/
protected void addFeatures(Iterable<FeatureDetails> featureDetails) {
for (FeatureDetails featureDetail : featureDetails) {
addFeature(featureDetail);
}
}
protected void getFeatureLogFromAgent(AgentDetails agent, FeatureDetails feature,
String relativeLogPath, OutputStream os) throws Exception {
if (!isSupportedAgent(agent)) {
return;
}
URI uri = createRequestURI(agent, feature, relativeLogPath);
RestGridClient client = new RestGridClient();
client.setRootUri(uri, false);
InputStream logStream = new BufferedInputStream(client.getInputStream());
byte[] buf = new byte[4096];
int len = 0;
while ((len = logStream.read(buf)) != -1) {
os.write(buf, 0, len);
}
}
private boolean isSupportedAgent(AgentDetails agent) {
if (!"mop".equals(agent.getContainerType().toLowerCase())) {
LOG.info("Unsupported agent type " + agent.getContainerType());
return false;
}
if (agent.getHref() == null) {
LOG.info("Agent href is null, no log can be retrieved");
return false;
}
return true;
}
private URI createRequestURI(AgentDetails agent, FeatureDetails feature, String relativeLogPath) {
UriBuilder ub = UriBuilder.fromUri(agent.getHref());
if ("mop".equals(agent.getContainerType().toLowerCase())) {
ub.path("directory");
}
//else if ("karaf".equals(agent.getContainerType().toLowerCase())) {
// ub.path("instance"); ?
//}
return ub.path(feature.getId().replace(':', '_')).path(relativeLogPath).build();
}
protected List<LogRecord> getFeatureLogRecordsFromAgent(AgentDetails agent, FeatureDetails feature,
String relativeLogPath, String queryName, String queryValue) throws Exception {
return getFeatureLogRecordsFromAgent(agent, feature, relativeLogPath,
Collections.singletonMap(queryName, Collections.singletonList(queryValue)));
}
protected List<LogRecord> getFeatureLogRecordsFromAgent(AgentDetails agent, FeatureDetails feature,
String relativeLogPath, Map<String, List<String>> queries) throws Exception {
if (!isSupportedAgent(agent)) {
return Collections.emptyList();
}
URI uri = createRequestURI(agent, feature, relativeLogPath);
RestGridClient client = new RestGridClient(uri);
return client.getLogRecords(queries);
}
/**
* Asserts that all the requested features have been provisioned properly
*/
protected void assertProvisioned() {
long start = System.currentTimeMillis();
Set<String> provisionedFeatures = new TreeSet<String>();
Set<String> failedFeatures = null;
while (true) {
failedFeatures = new TreeSet<String>();
long now = System.currentTimeMillis();
try {
ProfileStatus profileStatus = getGridClient().getProfileStatus(profileId);
if (profileStatus != null) {
List<DependencyStatus> dependencyStatus = profileStatus.getFeatures();
for (DependencyStatus status : dependencyStatus) {
String featureId = status.getFeatureId();
if (status.isProvisioned()) {
if (provisionedFeatures.add(featureId)) {
LOG.info("Provisioned feature: " + featureId);
}
} else {
failedFeatures.add(featureId);
}
}
}
if (failedFeatures.isEmpty()) {
return;
}
} catch (URISyntaxException e) {
LOG.warn("Failed to poll profile status: " + e, e);
}
long delta = now - start;
if (delta > startupTimeout) {
Assert.fail("Provision failure. Not enough instances of features: "
+ failedFeatures + " after waiting " + (startupTimeout / 1000) + " seconds");
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ignore
}
}
}
}
}