TestRunner.java

/***************************************************************************
   Copyright 2014 Emily Estes

   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 net.metanotion.util;


import java.io.IOException;
import java.io.FileInputStream;
import java.io.InputStreamReader;
//import java.io.PrintStream;
import java.io.Reader;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** A class to run a series of tests and check their results.
<p>Shouldn't I just use a unit testing framework? Yes, I should. This is probably a big waste of time. Normally
you shouldn't admit something like this, much less leave it in the prominent JavaDoc comment at the top of the
class. This isn't pointless wheel reinvention. This just automating something dumb. This isn't meant to replace
unit testing, or integration testing or anything. This is a convenience class to automate something really simple.</p>

<p>The basic idea is that you give this class an .INI file which lists a series of tests in the sections. Each
section contains the following keys: ClassName, arg.0 to arg.N (for each command line argument), and (optionally)
OutputCheck which is the name of a file to compare the contents of the standard output to. If it doesn't the output
is printed along with the name of the section in the .INI file.</p>

<p>The only argument to this class is the filename of the INI file.</p>
*/
public final class TestRunner {
	private static final Logger logger = LoggerFactory.getLogger(TestRunner.class);

	private static String eval(final String arg) {
		final Pattern p = Pattern.compile("\\$\\{[^\\}]*}");
		final Matcher m = p.matcher(arg);
		final StringBuffer sb = new StringBuffer();
		int nextStart = 0;
		while(m.find()) {
			final int start = m.start();
			sb.append(arg.substring(nextStart, start).replaceAll("\\\\\\{", "{").replaceAll("\\\\\\\\", "\\\\"));
			nextStart = m.end();
			final String v = arg.substring(start, nextStart);
			sb.append(System.getProperty(v.substring(2,v.length() -1)));
		}
		sb.append(arg.substring(nextStart).replaceAll("\\\\\\{", "{").replaceAll("\\\\\\\\", "\\\\"));
		return sb.toString();
	}

	private static IniProperties readFile(final String file) throws IOException {
		try (final Reader r = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
			return new IniProperties().load(r);
		}
	}

	/** Start running tests. The test are configured in an INI file whose filename should be in the first element of
		the array of arguments.
		@param args The command line arguments.
	*/
	public static void main(final String[] args) {
		try {
			System.getProperties().list(System.out);
			final IniProperties ini = readFile(args[0]);
			//final PrintStream stdout = System.out;
			final Iterator<String> sections = ini.sections();
			while(sections.hasNext()) {
				final String s = sections.next();
				if("".equals(s)) { continue; }
				logger.info("Running Test '{}':", s);
				final String output = ini.getProperty(ini.getProperty("Output", s));
				final ArrayList<String> argList = new ArrayList<>();
				int i=0;
				String a = ini.getProperty("arg." + i, s);
				while(a != null) {
					final String arg = eval(a);
					logger.info("Argument '{}'", arg);
					argList.add(arg);
					i++;
					a = ini.getProperty("arg." + i, s);
				}
				final String className = ini.getProperty("ClassName", s);
				if(className == null) {
					final String executable = eval(ini.getProperty("Executable", s));
					argList.add(0, executable);
					final ProcessBuilder exec = new ProcessBuilder(argList);
					exec.inheritIO();
					final Process process = exec.start();
					final int exitCode = process.waitFor();
					if(exitCode != 0) {
						throw new RuntimeException("Process " + executable + " terminated with condition "
							+ Integer.toString(exitCode));
					}
				} else {
					final Class test = Class.forName(eval(className));
					final Method main = test.getMethod("main", String[].class);
					final String[] parameter = argList.toArray(new String[argList.size()]);
					final Object[] argArray = { parameter };
					main.invoke(null, argArray);
				}
			}
		/** In theory no checked Exception will be thrown here from the method signatures, but the invokation of main
		above may actually throw any checked Exception, so we need to catch it to denote that the test failed, and log
		it. */
		} catch (final Exception e) {
			logger.debug("Failed Test", e);
			System.exit(-1);
		}
	}
}