/*
* Copyright 2012 Splunk, Inc.
*
* 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 com.splunk;
import junit.framework.TestCase;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Base test case for SDK test suite.
*
* TestCase does the following actions in the various test contexts:
*
* @@Before:
* - Read ~/.splunkrc to get host, port, user, and password to connect with.
* - Create a Service object connected to the Splunk instance.
*
* @@After:
* - Logout the Service object.
*/
public abstract class SDKTestCase {
protected static final boolean WORKAROUND_KNOWN_BUGS = true;
private static final boolean VERBOSE_PORT_SCAN = false;
protected static Service service;
protected List<String> installedApps;
protected Command command;
public static String streamToString(java.io.InputStream is) {
Reader r = null;
try {
r = new InputStreamReader(is, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Your JVM does not support UTF-8!");
}
StringBuffer sb = new StringBuffer();
int ch;
try {
while ((ch = r.read()) != -1) {
sb.append((char)ch);
}
} catch (IOException ioex) {
throw new RuntimeException(ioex.getMessage());
}
return sb.toString();
}
public void connect() {
if (service != null) {
service.login(service.username, service.password);
} else {
service = Service.connect(command.opts);
}
}
@Before
public void setUp() throws Exception {
// If using Charles Proxy for debugging, uncomment these lines.
//System.setProperty("https.proxyHost", "127.0.0.1");
//System.setProperty("https.proxyPort", "8888");
command = Command.splunk();
connect();
if (restartRequired()) {
System.out.println(
"WARNING: Splunk was already in a state requiring a " +
"restart prior to running this test. Trying to recover...");
splunkRestart();
System.out.println("Restart complete.");
}
installedApps = new ArrayList<String>();
}
@After
public void tearDown() throws Exception {
if (restartRequired()) {
Assert.fail("Test left Splunk in a state that required restart.");
}
// Cannot delete applications in Splunk 4.2.
if (service.versionIsAtLeast("4.3")) {
// We delete the apps we installed for capabilities after checking
// for restarts that might be required, since deleting an app
// triggers a restart (but one that we can safely ignore).
for (String applicationName : installedApps) {
try {
service.getApplications().remove(applicationName);
} catch (HttpException e) {
// WORKAROUND (SPL-75224): Under Splunk 6.0 on Windows, deleting apps sometimes fails with
// the message "Operation completed successfully." The app is actually deleted, but it will
// cause tests to fail.
if (service.versionCompare("6.0.0") == 0 && e.getStatus() != 500) {
throw e;
}
}
clearRestartMessage();
}
}
}
// === Temporary Names ===
protected static String createTimestamp() {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd-HH:mm:ss");
return dateFormat.format(new Date());
}
protected static String createTemporaryName() {
return "delete-me-" + UUID.randomUUID().toString();
}
public static InputStream openResource(String path) {
if (path.startsWith("splunk_search:")) {
path = path.substring("splunk_search:".length());
String[] pathComponents = path.split("/");
String searchType = pathComponents[0];
String outputMode = pathComponents[1];
String search = pathComponents[2];
Args resultsArgs = new Args("output_mode", outputMode);
if (searchType.equals("blocking")) {
Job job = service.getJobs().create(
search,
new Args("exec_mode", "blocking"));
return job.getResults(resultsArgs);
}
else if (searchType.equals("oneshot")) {
return service.oneshotSearch(search, resultsArgs);
}
else {
throw new IllegalArgumentException(
"Unrecognized search type: " + searchType);
}
}
InputStream input = ResourceRoot.class.getResourceAsStream(path);
Assert.assertNotNull("Could not open " + path, input);
return input;
}
// === Asserts ===
public static abstract class EventuallyTrueBehavior {
public int tries;
public int pauseTime;
{
tries = 10;
pauseTime = 1000;
}
public String timeoutMessage = "Test timed out before true.";
public abstract boolean predicate();
}
public static boolean assertEventuallyTrue(EventuallyTrueBehavior behavior) {
int remainingTries = behavior.tries;
while (remainingTries > 0) {
boolean succeeded = behavior.predicate();
if (succeeded) {
return true;
} else {
remainingTries -= 1;
try {
Thread.sleep(behavior.pauseTime);
} catch (InterruptedException e) {}
}
}
Assert.fail(behavior.timeoutMessage);
return false;
}
// === Test Data Installation ===
public boolean hasTestData() {
String collectionName = "sdk-app-collection";
return service.getApplications().containsKey("sdk-app-collection");
}
public void installApplicationFromTestData(String applicationName) {
String collectionName = "sdk-app-collection";
if (!service.getApplications().containsKey("sdk-app-collection")) {
throw new TestDataNotInstalledException();
}
String splunkHome = service.getSettings().getSplunkHome();
// This is the filename separator sequence for splunkd, not
// the Splunk SDK. Therefore we can't just use File.separator here.
String separator;
if (splunkHome.contains("/") && splunkHome.contains("\\")) {
// Windows - allows mixed paths
separator = "\\";
} else if (splunkHome.contains("/")) {
// Unix or Mac OS X
separator = "/";
} else if (splunkHome.contains("\\")) {
// Windows
separator = "\\";
} else {
throw new RuntimeException(
"Couldn't determine what the path separator was " +
"for splunkd.");
}
String[] pathComponents = {splunkHome, "etc", "apps",
collectionName, "build", applicationName + ".tar"};
String appPath = Util.join(separator, pathComponents);
Args args = new Args();
args.put("name", appPath);
args.put("update", "1");
service.post("apps/appinstall", args);
installedApps.add(applicationName);
}
// === Restarts ===
public void clearRestartMessage() {
final MessageCollection messages = service.getMessages();
if (messages.containsKey("restart_required")) {
messages.remove("restart_required");
assertEventuallyTrue(new EventuallyTrueBehavior() {
@Override public boolean predicate() {
messages.refresh();
return !messages.containsKey("restart_required");
}
});
}
}
public boolean restartRequired() {
for (Message message : service.getMessages().values()) {
if (message.containsKey("restart_required")) {
return true;
}
}
return false;
}
private void markAsRestartRequired() {
service.getMessages().create(
"restart_required",
"Unit test called markAsRestartRequired().");
}
public void splunkRestart() {
if (!restartRequired()) {
Assert.fail("Asked to restart Splunk when no restart was required.");
}
uncheckedSplunkRestart();
}
public void uncheckedSplunkRestart() {
ResponseMessage response = service.restart();
if (response.getStatus() != 200) {
Assert.fail("Restart command failed: " + response.getContent());
}
// (This status will be cleared when Splunk actually restarts.)
markAsRestartRequired();
// Wait for splunkd to come back up fresh.
// (And update 'service'.)
assertEventuallyTrue(new EventuallyTrueBehavior() {
{
tries = 20;
pauseTime = 1000;
}
@Override
public boolean predicate() {
try {
connect();
return !restartRequired();
} catch (Exception e) {
return false;
}
}
});
}
// === Misc ===
protected static void sleep(int milliseconds) {
try {
Thread.sleep(milliseconds);
}
catch (InterruptedException e) {}
}
protected int findNextUnusedPort(int startingPort) {
int port = startingPort;
while (isPortInUse(port)) {
port++;
}
return port;
}
public boolean isPortInUse(int port) {
try {
Socket pingSocket = new Socket();
// On Windows, the firewall doesn't respond at all if you connect to an unbound port, so we need to
// take lack of a connection as an empty port. Timeout is 1000ms.
try {
pingSocket.connect(new InetSocketAddress(service.getHost(), port), 1000);
} catch (SocketTimeoutException ste) {
return false;
}
pingSocket.close();
if (VERBOSE_PORT_SCAN) {
System.out.println("IN-USE(" + port + ")");
}
return true;
} catch (IOException e) {
if (VERBOSE_PORT_SCAN) {
System.out.println("OPEN(" + port + "): " + e.getMessage());
}
return false;
}
}
protected static boolean contains(String[] array, String value) {
return Arrays.asList(array).contains(value);
}
protected static void assertEquals(String[] a1, String[] a2) {
Assert.assertArrayEquals(a1, a2);
}
protected static String locateSystemLog() {
String filename = null;
String osName = service.getInfo().getOsName();
if (osName.equals("Windows"))
filename = "C:\\Windows\\WindowsUpdate.log";
else if (osName.equals("Linux")) {
filename = "/etc/fstab";
}
else if (osName.equals("Darwin")) {
filename = "/var/log/system.log";
} else {
throw new RuntimeException("OS " + osName + " not recognized");
}
Assert.assertNotNull(filename);
return filename;
}
protected static String joinServerPath(String[] components) {
String separator;
String osName = service.getInfo().getOsName();
if (osName.equals("Windows"))
separator = "\\";
else if (osName.equals("Linux"))
separator = "/";
else if (osName.equals("Darwin")) {
separator = "/";
} else {
throw new RuntimeException("OS " + osName + " not recognized");
}
return Util.join(separator, components);
}
protected boolean firstLineIsXmlDtd(InputStream stream) {
InputStreamReader reader;
try {
reader = new InputStreamReader(stream, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error(e);
}
BufferedReader lineReader = new BufferedReader(reader);
try {
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>".equals(
lineReader.readLine()
);
} catch (IOException e) {
Assert.fail(e.toString());
return false;
}
}
}