/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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. */ /* * $Id$ */ package org.apache.qetest; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.util.Hashtable; /** * Base class for testing commandline driven products. * * This class provides a default algorithim for testing any * command line based tool. Subclasses define the * exact command line args, etc. used for different products. * Subclasses can also either shell an external process or can * just construct a class and call main(). * * @author Shane_Curcuru@us.ibm.com * @version $Id$ */ public abstract class ExecTestlet extends FileTestlet { /** * Parameter: Actual name of external program to call. */ public static final String OPT_PROGNAME = "progName"; /** * Timing data: how long process takes to exec. * Default is -1 to represent a bogus number. */ protected long timeExec = -1; /** * Default path/name of external program to call, OR * actual name of class to call. * @return foo, must be overridden. */ public abstract String getProgram(); /** * If the program should be shelled out or if it is a Java * class to call main on. * @return foo, must be overridden. */ public abstract boolean isExternal(); /** * Worker method to get list of arguments specific to this program. * *

Should construct whole list of arguments needed to call * this program, including any options and args needed to * process the files in the datalet. Must be overriden.

* *

If isExternal is true, this should presumably put the * name of the program first, since we just shell that as a * command line. If isExternal is false, this should not * include the Java classname.

* * @param datalet that defined the test data * @return String array of arguments suitable to pass to * Runtime.exec() or main() */ public abstract String[] getArguments(FileDatalet datalet); /** * Worker method to actually perform the test; * overriden to use command line processing. * * Logs out applicable info; attempts to perform transformation. * * @param datalet to test with * @throws allows any underlying exception to be thrown */ protected void testDatalet(FileDatalet datalet) throws Exception { String[] args = getArguments(datalet); StringBuffer argBuf = new StringBuffer(); for (int i = 0; i < args.length; i++) { argBuf.append(args[i]); argBuf.append(" "); } logger.logMsg(Logger.TRACEMSG, "testDatalet executing: " + argBuf.toString()); // Use one of two worker methods to execute the process, either // by shelling an external process, or by constructing the // Java object and then calling main() if (isExternal()) execProcess(datalet, args); else execMain(datalet, args); } /** * Worker method to call a Java class' main() method. * *

Simply calls a no-arg constructor and then passes the * args to the main() method. May be overridden.

* * @param datalet that defined the test data * @param cmdline actual command line to run, including program name * @param environment passed as-is to Process.run * @return return value from program * @exception Exception may be thrown by Runtime.exec */ public void execMain(FileDatalet datalet, String[] cmdline) throws Exception { // Default implementation; may be overriden Class clazz = Class.forName(getProgram()); if (null == clazz) { logger.checkErr("Can't find classname: " + getProgram()); return; } try { // ...find the main() method... Class[] parameterTypes = new Class[1]; parameterTypes[0] = java.lang.String[].class; Method main = clazz.getMethod("main", parameterTypes); // ...and execute the method! Object[] mainArgs = new Object[1]; mainArgs[0] = cmdline; final long startTime = System.currentTimeMillis(); main.invoke(null, mainArgs); timeExec = System.currentTimeMillis() - startTime; // Also log out a perf element by default Hashtable attrs = new Hashtable(); attrs.put("program", getProgram()); attrs.put("isExternal", "false"); attrs.put("timeExec", new Long(timeExec)); logPerf(datalet, attrs); attrs = null; } catch (Throwable t) { logger.logThrowable(Logger.ERRORMSG, t, "Javaclass.main() threw"); logger.checkErr(getProgram() + ".main() threw: " + t.toString()); } } /** * Worker method to shell out an external process. * *

Does a simple capturing of the out and err streams from * the process and logs them out. Inherits the same environment * that the current JVM is in. No need to override

* * @param datalet that defined the test data * @param cmdline actual command line to run, including program name * @param environment passed as-is to Process.run * @return return value from program * @exception Exception may be thrown by Runtime.exec */ public void execProcess(FileDatalet datalet, String[] cmdline) throws Exception { if ((cmdline == null) || (cmdline.length < 1)) { logger.checkFail("execProcess called with null/blank arguments!"); return; } int bufSize = 2048; // Arbitrary bufSize seems to work well ThreadedStreamReader outReader = new ThreadedStreamReader(); ThreadedStreamReader errReader = new ThreadedStreamReader(); Runtime r = Runtime.getRuntime(); java.lang.Process proc = null; // Actually begin executing the program logger.logMsg(Logger.TRACEMSG, "execProcess starting " + cmdline[0]); //@todo Note: we should really provide a way for the datalet // to specify any additional environment needed for the // second arg to exec(); String[] environment = null; final long startTime = System.currentTimeMillis(); proc = r.exec(cmdline, environment); // Immediately begin capturing any output therefrom outReader.setInputStream( new BufferedReader( new InputStreamReader(proc.getInputStream()), bufSize)); errReader.setInputStream( new BufferedReader( new InputStreamReader(proc.getErrorStream()), bufSize)); // Start two threads off on reading the System.out and System.err from proc outReader.start(); errReader.start(); int processReturnVal = -2; // HACK the default try { // Wait for the process to exit normally processReturnVal = proc.waitFor(); // Record time we finally rejoin, i.e. when the process is done timeExec = System.currentTimeMillis() - startTime; } catch (InterruptedException ie1) { logger.logThrowable(Logger.ERRORMSG, ie1, "execProcess proc.waitFor() threw"); } // Now that we're done, presumably the Readers are also done StringBuffer sysOut = null; StringBuffer sysErr = null; try { outReader.join(); sysOut = outReader.getBuffer(); } catch (InterruptedException ie2) { logger.logThrowable(Logger.ERRORMSG, ie2, "Joining outReader threw"); } try { errReader.join(); sysErr = errReader.getBuffer(); } catch (InterruptedException ie3) { logger.logThrowable(Logger.ERRORMSG, ie3, "Joining errReader threw"); } logAndCheckStreams(datalet, cmdline, sysOut, sysErr, processReturnVal); } /** * Worker method to evaluate the System.out/.err streams of * a particular processor. * * Logs out the streams if available, then calls a worker method * to actually call check() if specific validation needed. * * @param datalet that defined the test data * @param cmdline that was used for execProcess * @param outBuf buffer from execProcess' System.out * @param errBuf buffer from execProcess' System.err * @param processReturnVal from execProcess */ protected void logAndCheckStreams(FileDatalet datalet, String[] cmdline, StringBuffer outBuf, StringBuffer errBuf, int processReturnVal) { Hashtable attrs = new Hashtable(); attrs.put("program", cmdline[0]); attrs.put("returnVal", String.valueOf(processReturnVal)); StringBuffer buf = new StringBuffer(); if ((null != errBuf) && (errBuf.length() > 0)) { buf.append(""); buf.append(errBuf); buf.append("\n"); } if ((null != outBuf) && (outBuf.length() > 0)) { buf.append(""); buf.append(outBuf); buf.append("\n"); } logger.logElement(Logger.INFOMSG, "checkOutputStreams", attrs, buf.toString()); buf = null; // Also log out a perf element by default attrs = new Hashtable(); attrs.put("program", cmdline[0]); attrs.put("isExternal", "true"); attrs.put("timeExec", new Long(timeExec)); logPerf(datalet, attrs); attrs = null; // Also call worker method to allow subclasses to // override checking of the output streams, as available checkStreams(datalet, cmdline, outBuf, errBuf, processReturnVal); } /** * Worker method to validate the System.out/.err streams. * * Default implementation does nothing; override if you wish * to actually validate the specific streams. * * @param datalet that defined the test data * @param cmdline that was used for execProcess * @param outBuf buffer from execProcess' System.out * @param errBuf buffer from execProcess' System.err * @param processReturnVal from execProcess */ protected void checkStreams(FileDatalet datalet, String[] cmdline, StringBuffer outBuf, StringBuffer errBuf, int processReturnVal) { // Default impl is no-op return; } /** * Worker method to write performance data in standard format. * * Writes out a perf elem with standardized idref, testlet, * input/output, and fileSize params. * * @param datalet to use for idref, etc. * @param hash of extra attributes to log. */ protected void logPerf(FileDatalet datalet, Hashtable hash) { if (null == hash) hash = new Hashtable(); File f = new File(datalet.getInput()); hash.put("idref", f.getName()); hash.put("input", datalet.getInput()); hash.put("output", datalet.getOutput()); hash.put("testlet", thisClassName); try { // Attempt to store size of input file, since overall // amount of data affects performance hash.put("fileSize", new Long(f.length())); } catch (Exception e) { hash.put("fileSize", "threw: " + e.toString()); } logger.logElement(Logger.STATUSMSG, "perf", hash, getCheckDescription(datalet)); } } // end of class ExecTestlet