ServiceTest.java [src/csip/test] Revision: Date:
/*
* $Id$
*
* This file is part of the Cloud Services Integration Platform (CSIP),
* a Model-as-a-Service framework, API and application suite.
*
* 2012-2022, Olaf David and others, OMSLab, Colorado State University.
*
* OMSLab licenses this file to you under the MIT license.
* See the LICENSE file in the project root for more information.
*/
package csip.test;
import csip.utils.Client;
import csip.utils.JSONUtils;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.Reader;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.logging.StreamHandler;
import org.apache.commons.io.FileUtils;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONObject;
import org.skyscreamer.jsonassert.JSONAssert;
/**
* Service Testing.
*
* @author odavid
*/
public class ServiceTest {
static final String KEYSONLY = "keysonly";
static final String KEYVALUE = "keyvalue";
static final String KEYVALUEORDER = "keyvalueorder";
static final String NONE = "none";
static final int NO_WARMUP = 0;
static final int WARMUP_AND_RUN = 1;
static final int WARMUP_ONLY = 2;
static final List<String> VALIDTESTING = Arrays.asList(KEYSONLY, KEYVALUE, KEYVALUEORDER, NONE);
File folder;
File[] files;
String url;
String testing;
int concurrent;
int workers;
int timeout;
int modelTimeout;
int delay;
int limit;
int warmup;
int warmupRuns;
boolean downloadFiles;
String logLevel = "WARNING";
String config;
boolean ignore = false;
static final Logger log = Logger.getLogger("ServiceTest");
static {
log.setLevel(Level.SEVERE);
SimpleFormatter fmt = new SimpleFormatter();
StreamHandler sh = new StreamHandler(System.out, fmt);
log.addHandler(sh);
}
private ServiceTest(File target) throws Exception {
if (!target.exists()) {
throw new IllegalArgumentException("Not a folder or file: " + target);
}
if (target.getName().endsWith("req.json") && target.isFile()) {
folder = target.getParentFile();
files = new File[]{target};
} else if (target.isDirectory()) {
folder = target;
files = folder.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith("-req.json");
}
});
// sort the file names.
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File o1, File o2) {
int n1 = extractNumber(o1.getName());
int n2 = extractNumber(o2.getName());
if ((n1 == 0) || (n2 == 0)) {
return o1.compareTo(o2);
} else {
return n1 - n2;
}
}
// to do - could make char s and char e configurable
// and this would enable custom ways to extract a number
// from the file name for sorting
private int extractNumber(String name) {
int i = 0;
try {
int s = name.lastIndexOf('t') + 1;
int e = name.lastIndexOf('-');
if ((s < 0) || (e < 0)) {
s = name.lastIndexOf('_') + 1;
e = name.lastIndexOf('.');
}
String number = name.substring(s, e);
i = Integer.parseInt(number);
} catch (NumberFormatException nfe) {
i = 0;
}
return i;
}
});
} else {
throw new IllegalArgumentException("Invalid argument: " + target);
}
// load the service testing settings.
Properties p = loadProperties(folder);
testing = p.getProperty("verify", NONE); // strict, keys
if (!VALIDTESTING.contains(testing)) {
throw new RuntimeException("invalid testing method: " + testing);
}
ignore = Boolean.parseBoolean(p.getProperty("ignore", "false"));
url = p.getProperty("url");
if (url == null) {
String host = p.getProperty("host");
String path = p.getProperty("path");
if (host == null && path == null) {
throw new RuntimeException("'url' (or host/path) not found in " + new File(folder, "service.properties"));
}
url = host + path;
}
concurrent = Integer.parseInt(p.getProperty("concurrency", "1"));
if (concurrent < 1) {
concurrent = 1;
}
timeout = Integer.parseInt(p.getProperty("timeout", "3600"));
if (timeout < 1000) {
timeout = 1000;
}
modelTimeout = Integer.parseInt(p.getProperty("modelTimeout", "600000")); // default is 10 minutes
if (modelTimeout <= 0) {
modelTimeout = 600000;
}
delay = Integer.parseInt(p.getProperty("delay", "0"));
if (delay < 0) {
delay = 0;
}
workers = Integer.parseInt(p.getProperty("workers", "1"));
if (workers < 0) {
workers = 1;
}
// warmup phase: 0-off, 1-on
warmup = Integer.parseInt(p.getProperty("warmup", "0"));
if (warmup < 0) {
warmup = NO_WARMUP;
}
warmupRuns = Integer.parseInt(p.getProperty("warmupRuns", "4"));
if (warmupRuns < 0) {
warmupRuns = 4;
}
downloadFiles = Boolean.parseBoolean(p.getProperty("download_files", "true"));
logLevel = p.getProperty("logLevel", "WARNING");
if ((logLevel != null) && (logLevel.length() > 0)) {
log.setLevel(Level.parse(logLevel));
}
config = p.getProperty("config", "service-conf.json");
limit = Integer.parseInt(p.getProperty("limit", "-1"));
limit = (limit < 1) ? files.length : limit;
}
/**
* Execute the test.
*
* @return
*/
private Results test() {
final Results results = new Results();
if (ignore) {
results.incIgnored();
return results;
}
try (Client client = new Client(timeout, log)) {
// apply configuration
if (config != null) {
// changed with absolute name
File conf = new File(config);
if (!conf.exists()) {
// relative name
conf = new File(folder, config);
}
if (conf.exists()) {
try {
JSONObject o = new JSONObject(FileUtils.readFileToString(conf, "UTF-8"));
client.doPUT(getBaseURLContext(url) + "/c/conf", o);
} catch (Exception E) {
log.warning("Failed to load config: " + conf);
}
} else {
// if there is no config... then assume service is pre-configured...
}
}
int maxFiles = limit;
final CountDownLatch latch = new CountDownLatch(maxFiles);
final CountDownLatch warmupLatch = new CountDownLatch(workers * warmupRuns);
final ConcurrentLinkedQueue<File> fileQueue = new ConcurrentLinkedQueue<>();
final ConcurrentLinkedQueue<File> warmups = new ConcurrentLinkedQueue<>();
for (int i = 0; i < maxFiles; i++) {
if ((i < workers * warmupRuns) && (warmup != NO_WARMUP)) {
warmups.add(files[i]);
}
fileQueue.add(files[i]);
}
ExecutorService warmupExecutor = null;
final ExecutorService executor = Executors.newFixedThreadPool(concurrent, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "exeuctor-thread");
}
});
// final ExecutorService executor = new ThreadPoolExecutor(
// concurrent, concurrent, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()) {
// @Override
// protected void afterExecute(Runnable r, Throwable t) {
// super.afterExecute(r, t);
// if (t != null) {
// System.out.println("Perform exception handler logic");
// }
// final File file = fileQueue.poll();
// if (file != null) {
// if (delay > 0) {
// try {
// System.out.println("THIS WORKER SLEEP FOR:" + delay);
// Thread.sleep(delay);
// } catch (InterruptedException ex) {
// System.out.println("Interruption during sleep - non-init!!!");
// }
// }
// // keep going
// submit(new Callable<Void>() {
// @Override
// public Void call() {
// return t(file, results, client, latch);
// }
// });
// }
// else
// {
// System.out.println("THE FILE IS NULL - THIS SHOULD NOT HAPPEN UNTIL END, filequeue-size=" + fileQueue.size());
// }
// }
// };
// submit the first run twice as a "warmup" across the workers
if (warmup != NO_WARMUP) {
final Results warmupResults = new Results();
warmupExecutor = Executors.newFixedThreadPool(workers * warmupRuns, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "warmup-thread");
}
});
for (int i = 0; i < (workers * warmupRuns); i++) {
final File file = warmups.poll();
log.info("SUBMITTING WARM UP RUN=" + file);
warmupExecutor.submit(new Callable<Void>() {
@Override
public Void call() {
return t(file, warmupResults, client, warmupLatch);
}
});
}
}
// submit the initial runs.
if (warmup != WARMUP_ONLY) {
// for (int i = 0; i < Math.min(concurrent, maxFiles); i++) {
for (int i = 0; i < maxFiles; i++) {
if (delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException ex) {
log.warning("Interrupted during sleep- init runs!");
}
}
final File file = fileQueue.poll();
log.info("SUBMITTING TO PROCESS FILE=" + file);
executor.submit(new Callable<Void>() {
@Override
public Void call() {
return t(file, results, client, latch);
}
});
}
}
try {
if (warmup != WARMUP_ONLY) {
latch.await();
}
if (warmup != NO_WARMUP) {
warmupLatch.await();
}
} catch (InterruptedException ex) {
// whatever
}
executor.shutdown();
if (warmup != NO_WARMUP) {
warmupExecutor.shutdown();
}
}
return results;
}
/**
*
* @param reqFile
* @param results
* @param client
* @param latch
* @return
*/
private Void t(File reqFile, Results results, Client client, CountDownLatch latch) {
try {
if (delay > 0) {
try {
Thread.sleep(delay);
} catch (InterruptedException ex) {
log.warning("Interrupted during sleep: pre-run-sleep");
}
}
File res = getResFile(reqFile);
// check if there is a async result with the status "Running"
if (res.exists()) {
res.delete();
// // ASYNC PART
// // check if the prev run was running.
// JSONObject prev_json = new JSONObject(FileUtils.readFileToString(res));
// if (JSONUtils.getStatus(prev_json).equals("Finished")) {
// log.warning("Skipped (Finished)" + " " + reqFile + "... ");
// results.incSkipped();
// log.info("EXITING test run,,, latch count-down?");
// return null;
// }
//
// if (JSONUtils.getStatus(prev_json).equals("Running") || JSONUtils.getStatus(prev_json).equals("Submitted")) {
// // it is running, need to check again.
// String q_url = getBaseURLContext(url) + "/q/" + JSONUtils.getSID(prev_json);
//
// // get the new
// client.doGET(q_url, res);
//
// // check the status again
// JSONObject this_json = new JSONObject(FileUtils.readFileToString(res));
// String status = JSONUtils.getStatus(this_json);
// log.info(status + " " + reqFile + "... ");
// if (status.equals("Finished")) {
//
// // dowload
// download(this_json, getResFolder(reqFile), client);
//
// if (testing.equals(NONE)) {
// results.incSuccess();
// log.info("ASYNC EXIT NONE");
// return null;
// }
// // test
// runtest(reqFile, this_json);
// }
// results.incSuccess();
// log.info("ASYNC EXIT END");
// return null;
// }
}
JSONObject req_json = new JSONObject(FileUtils.readFileToString(reqFile, "UTF-8"));
boolean isAsync = JSONUtils.isAsync(req_json);
// input
File inp = getInpFolder(reqFile);
File[] attachments = (inp != null) ? inp.listFiles() : new File[0];
// delete output if exist.
FileUtils.deleteDirectory(getResFolderName(reqFile));
// run the service
JSONObject response = client.doPOST(url, req_json, attachments, (Map<String, String>) null);
FileUtils.writeStringToFile(getResFile(reqFile), response.toString(2).replace("\\/", "/"), "UTF-8");
// print the error if failed.
if (JSONUtils.getStatus(response).equals("Failed")) {
JSONObject respose_metainfo = response.getJSONObject("metainfo");
throw new Exception(respose_metainfo.getString("error"));
}
if (isAsync) {
results.incSuccess();
log.info("THREAD EXECUTOR TEST SUCCESS:" + reqFile);
return null;
}
if (!JSONUtils.hasResult(response)) {
throw new Exception("No 'result' in response");
}
if (downloadFiles) {
download(response, getResFolder(reqFile), client);
}
if (testing.equals(NONE)) {
results.incSuccess();
log.info("EXITING test run,,, latch count-down?");
return null;
}
runtest(reqFile, response);
results.incSuccess();
} catch (Exception | Error E) {
System.out.println(" Failed: " + reqFile);
log.warning("ERROR-" + E.toString());
for (StackTraceElement ste : E.getStackTrace()) {
log.warning(ste.getClassName() + ":" + ste.getMethodName() + ":" + ste.getLineNumber() + "---" + ste.toString());
}
System.out.println(E.getMessage());
results.incFailed();
} finally {
log.info("THREAD EXECUTOR COUNT DOWN AFTER:" + reqFile);
latch.countDown();
}
return null;
}
private void runtest(File reqFile, JSONObject response) throws Exception {
JSONArray response_result = response.getJSONArray("result");
// all real checking starts here.
// golden response
JSONObject golJson = new JSONObject(FileUtils.readFileToString(getResponse(reqFile), "UTF-8"));
// check output files
JSONArray response_golden = golJson.getJSONArray("result");
Map<String, JSONObject> rmap = JSONUtils.preprocess(response_result);
Map<String, JSONObject> golmap = JSONUtils.preprocess(response_golden);
for (String key : rmap.keySet()) {
JSONObject val = rmap.get(key);
String url = val.getString("value");
if (url.startsWith("http") && url.endsWith(key)) {
response_result.remove(val);
golmap.remove(golmap.get(key));
}
}
File out = getRefFolder(reqFile);
if (out != null) {
File[] outFiles = out.listFiles();
for (File file : outFiles) {
String f = file.getName();
if (!rmap.containsKey(f)) {
throw new Exception("Expecting file in result: " + f);
}
// TODO compare the output files.
}
}
switch (testing) {
case KEYVALUE:
compareStrict(response_result, response_golden);
break;
case KEYSONLY:
compareKeys(response_result, response_golden);
break;
default:
throw new IllegalArgumentException(testing);
}
}
/**
*
*/
public static class Results {
int succeeded = 0;
int failed = 0;
int skipped = 0;
int ignored = 0;
void add(Results other) {
succeeded += other.getSucceeded();
failed += other.getFailed();
skipped += other.getSkipped();
ignored += other.getIgnored();
}
public int getTotal() {
return succeeded + failed + skipped + ignored;
}
public int getFailed() {
return failed;
}
public int getSkipped() {
return skipped;
}
public int getSucceeded() {
return succeeded;
}
public int getIgnored() {
return ignored;
}
synchronized void incSuccess() {
succeeded++;
}
synchronized void incFailed() {
failed++;
}
synchronized void incSkipped() {
skipped++;
}
synchronized void incIgnored() {
ignored++;
}
@Override
public String toString() {
return "=================" + "\n"
+ " Total: " + getTotal() + "\n"
+ " Failed: " + getFailed() + "\n"
+ " Succeeded: " + getSucceeded() + "\n"
+ " Skipped: " + getSkipped() + "\n"
+ " Ignored: " + getIgnored() + "\n";
}
}
static void download(JSONObject resp, File resultFolder, Client client) throws Exception {
JSONArray arr = resp.getJSONArray("result");
// download files using the 'q' service.
Map<String, JSONObject> rmap = JSONUtils.preprocess(arr);
for (String key : rmap.keySet()) {
JSONObject val = rmap.get(key);
String url = val.getString("value");
if (url == null) {
throw new Exception("Expecting file in result: " + key);
}
if (url.startsWith("http") && url.contains("/q/") && url.endsWith(key)) {
if (!resultFolder.exists()) {
resultFolder.mkdirs();
}
File outFile = new File(resultFolder, key);
client.doGET(url, outFile);
if (!outFile.exists()) {
throw new Exception("Missing output file: " + key);
}
}
}
}
static File getInpFolder(File request) {
return getFolder(request, "-req");
}
static File getRefFolder(File request) {
return getFolder(request, "-ref");
}
static File getResFolderName(File request) {
String name = request.getPath();
return new File(name.replace("-req.json", "-res"));
}
static File getResponse(File request) {
String name = request.getPath();
if (!name.endsWith("-req.json")) {
return null;
}
File response = new File(name.replace("-req.json", "-res.json"));
if (!response.exists() || !request.canRead() || !response.canRead()) {
return null;
}
return response;
}
static File getResFile(File request) {
String name = request.getPath();
File res = new File(name.replace("-req.json", "-res.json"));
return res;
}
static File getResFolder(File request) {
String name = request.getPath();
File res = new File(name.replace("-req.json", "-res"));
return res;
}
static File getFolder(File request, String postfix) {
String name = request.getPath();
if (!name.endsWith("-req.json")) {
return null;
}
File inp = new File(name.replace("-req.json", postfix));
if (!inp.exists() || !inp.canRead() || !inp.isDirectory()) {
return null;
}
return inp;
}
static void compareKeys(JSONArray res, JSONArray gol) throws Exception {
Map<String, JSONObject> preq = JSONUtils.preprocess(res);
Map<String, JSONObject> pgol = JSONUtils.preprocess(gol);
for (String key : pgol.keySet()) {
if (!preq.containsKey(key)) {
throw new Exception("Missing key in result:" + key);
}
preq.remove(key);
}
if (!preq.keySet().isEmpty()) {
throw new Exception("Extra key(s) in result:" + preq.keySet().toString());
}
}
static void compareStrict(JSONArray res, JSONArray gol) throws Exception {
JSONAssert.assertEquals(gol.toString(), res.toString(), false);
}
static Properties loadProperties(File folder) throws Exception {
Properties p = new Properties();
File props = new File(folder.getParentFile(), "service.properties");
if (props.exists()) {
try (Reader is = new FileReader(props)) {
p.load(is);
}
}
props = new File(folder, "service.properties");
if (!props.exists()) {
throw new RuntimeException("Not found: " + props);
}
try (Reader is = new FileReader(props)) {
p.load(is);
}
return p;
}
static String getBaseURLContext(String url) {
// this is the first part (host + port)
String host = url.substring(0, url.indexOf("/", url.indexOf("://") + 3));
// this is the second part (context)
String context = url.substring(host.length() + 1, url.indexOf("/", host.length() + 1));
return host + "/" + context;
}
public static Results run(String target) throws Exception {
return new ServiceTest(new File(target)).test();
}
public static Results run(String[] targets) throws Exception {
Results summary = new Results();
for (String target : targets) {
Results result = new ServiceTest(new File(target)).test();
summary.add(result);
}
return summary;
}
public static void main(String[] args) throws Exception {
System.out.println(run(args));
}
}