1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 /* 19 * $Id$ 20 */ 21 22 package org.apache.qetest; 23 24 import java.io.BufferedReader; 25 import java.io.File; 26 import java.io.InputStreamReader; 27 import java.lang.reflect.Method; 28 import java.util.Hashtable; 29 30 /** 31 * Base class for testing commandline driven products. 32 * 33 * This class provides a default algorithim for testing any 34 * command line based tool. Subclasses define the 35 * exact command line args, etc. used for different products. 36 * Subclasses can also either shell an external process or can 37 * just construct a class and call main(). 38 * 39 * @author Shane_Curcuru@us.ibm.com 40 * @version $Id$ 41 */ 42 public abstract class ExecTestlet extends FileTestlet 43 { 44 /** 45 * Parameter: Actual name of external program to call. 46 */ 47 public static final String OPT_PROGNAME = "progName"; 48 49 /** 50 * Timing data: how long process takes to exec. 51 * Default is -1 to represent a bogus number. 52 */ 53 protected long timeExec = -1; 54 55 /** 56 * Default path/name of external program to call, OR 57 * actual name of class to call. 58 * @return foo, must be overridden. 59 */ getProgram()60 public abstract String getProgram(); 61 62 /** 63 * If the program should be shelled out or if it is a Java 64 * class to call main on. 65 * @return foo, must be overridden. 66 */ isExternal()67 public abstract boolean isExternal(); 68 69 /** 70 * Worker method to get list of arguments specific to this program. 71 * 72 * <p>Should construct whole list of arguments needed to call 73 * this program, including any options and args needed to 74 * process the files in the datalet. Must be overriden.</p> 75 * 76 * <p>If isExternal is true, this should presumably put the 77 * name of the program first, since we just shell that as a 78 * command line. If isExternal is false, this should <b>not</b> 79 * include the Java classname.</p> 80 * 81 * @param datalet that defined the test data 82 * @return String array of arguments suitable to pass to 83 * Runtime.exec() or main() 84 */ getArguments(FileDatalet datalet)85 public abstract String[] getArguments(FileDatalet datalet); 86 87 /** 88 * Worker method to actually perform the test; 89 * overriden to use command line processing. 90 * 91 * Logs out applicable info; attempts to perform transformation. 92 * 93 * @param datalet to test with 94 * @throws allows any underlying exception to be thrown 95 */ testDatalet(FileDatalet datalet)96 protected void testDatalet(FileDatalet datalet) 97 throws Exception 98 { 99 String[] args = getArguments(datalet); 100 101 StringBuffer argBuf = new StringBuffer(); 102 for (int i = 0; i < args.length; i++) 103 { 104 argBuf.append(args[i]); 105 argBuf.append(" "); 106 } 107 logger.logMsg(Logger.TRACEMSG, "testDatalet executing: " + argBuf.toString()); 108 109 // Use one of two worker methods to execute the process, either 110 // by shelling an external process, or by constructing the 111 // Java object and then calling main() 112 if (isExternal()) 113 execProcess(datalet, args); 114 else 115 execMain(datalet, args); 116 } 117 118 /** 119 * Worker method to call a Java class' main() method. 120 * 121 * <p>Simply calls a no-arg constructor and then passes the 122 * args to the main() method. May be overridden.</p> 123 * 124 * @param datalet that defined the test data 125 * @param cmdline actual command line to run, including program name 126 * @param environment passed as-is to Process.run 127 * @return return value from program 128 * @exception Exception may be thrown by Runtime.exec 129 */ execMain(FileDatalet datalet, String[] cmdline)130 public void execMain(FileDatalet datalet, String[] cmdline) 131 throws Exception 132 { 133 // Default implementation; may be overriden 134 Class clazz = Class.forName(getProgram()); 135 if (null == clazz) 136 { 137 logger.checkErr("Can't find classname: " + getProgram()); 138 return; 139 } 140 141 try 142 { 143 // ...find the main() method... 144 Class[] parameterTypes = new Class[1]; 145 parameterTypes[0] = java.lang.String[].class; 146 Method main = clazz.getMethod("main", parameterTypes); 147 148 // ...and execute the method! 149 Object[] mainArgs = new Object[1]; 150 mainArgs[0] = cmdline; 151 final long startTime = System.currentTimeMillis(); 152 main.invoke(null, mainArgs); 153 timeExec = System.currentTimeMillis() - startTime; 154 155 // Also log out a perf element by default 156 Hashtable attrs = new Hashtable(); 157 attrs.put("program", getProgram()); 158 attrs.put("isExternal", "false"); 159 attrs.put("timeExec", new Long(timeExec)); 160 logPerf(datalet, attrs); 161 attrs = null; 162 } 163 catch (Throwable t) 164 { 165 logger.logThrowable(Logger.ERRORMSG, t, "Javaclass.main() threw"); 166 logger.checkErr(getProgram() + ".main() threw: " + t.toString()); 167 } 168 } 169 170 /** 171 * Worker method to shell out an external process. 172 * 173 * <p>Does a simple capturing of the out and err streams from 174 * the process and logs them out. Inherits the same environment 175 * that the current JVM is in. No need to override</p> 176 * 177 * @param datalet that defined the test data 178 * @param cmdline actual command line to run, including program name 179 * @param environment passed as-is to Process.run 180 * @return return value from program 181 * @exception Exception may be thrown by Runtime.exec 182 */ execProcess(FileDatalet datalet, String[] cmdline)183 public void execProcess(FileDatalet datalet, String[] cmdline) 184 throws Exception 185 { 186 if ((cmdline == null) || (cmdline.length < 1)) 187 { 188 logger.checkFail("execProcess called with null/blank arguments!"); 189 return; 190 } 191 192 int bufSize = 2048; // Arbitrary bufSize seems to work well 193 ThreadedStreamReader outReader = new ThreadedStreamReader(); 194 ThreadedStreamReader errReader = new ThreadedStreamReader(); 195 Runtime r = Runtime.getRuntime(); 196 java.lang.Process proc = null; 197 198 // Actually begin executing the program 199 logger.logMsg(Logger.TRACEMSG, "execProcess starting " + cmdline[0]); 200 201 //@todo Note: we should really provide a way for the datalet 202 // to specify any additional environment needed for the 203 // second arg to exec(); 204 String[] environment = null; 205 final long startTime = System.currentTimeMillis(); 206 proc = r.exec(cmdline, environment); 207 208 // Immediately begin capturing any output therefrom 209 outReader.setInputStream( 210 new BufferedReader( 211 new InputStreamReader(proc.getInputStream()), bufSize)); 212 errReader.setInputStream( 213 new BufferedReader( 214 new InputStreamReader(proc.getErrorStream()), bufSize)); 215 216 // Start two threads off on reading the System.out and System.err from proc 217 outReader.start(); 218 errReader.start(); 219 int processReturnVal = -2; // HACK the default 220 try 221 { 222 // Wait for the process to exit normally 223 processReturnVal = proc.waitFor(); 224 // Record time we finally rejoin, i.e. when the process is done 225 timeExec = System.currentTimeMillis() - startTime; 226 } 227 catch (InterruptedException ie1) 228 { 229 logger.logThrowable(Logger.ERRORMSG, ie1, 230 "execProcess proc.waitFor() threw"); 231 } 232 233 // Now that we're done, presumably the Readers are also done 234 StringBuffer sysOut = null; 235 StringBuffer sysErr = null; 236 try 237 { 238 outReader.join(); 239 sysOut = outReader.getBuffer(); 240 } 241 catch (InterruptedException ie2) 242 { 243 logger.logThrowable(Logger.ERRORMSG, ie2, "Joining outReader threw"); 244 } 245 246 try 247 { 248 errReader.join(); 249 sysErr = errReader.getBuffer(); 250 } 251 catch (InterruptedException ie3) 252 { 253 logger.logThrowable(Logger.ERRORMSG, ie3, "Joining errReader threw"); 254 } 255 256 logAndCheckStreams(datalet, cmdline, sysOut, sysErr, processReturnVal); 257 } 258 259 260 /** 261 * Worker method to evaluate the System.out/.err streams of 262 * a particular processor. 263 * 264 * Logs out the streams if available, then calls a worker method 265 * to actually call check() if specific validation needed. 266 * 267 * @param datalet that defined the test data 268 * @param cmdline that was used for execProcess 269 * @param outBuf buffer from execProcess' System.out 270 * @param errBuf buffer from execProcess' System.err 271 * @param processReturnVal from execProcess 272 */ logAndCheckStreams(FileDatalet datalet, String[] cmdline, StringBuffer outBuf, StringBuffer errBuf, int processReturnVal)273 protected void logAndCheckStreams(FileDatalet datalet, String[] cmdline, 274 StringBuffer outBuf, StringBuffer errBuf, int processReturnVal) 275 { 276 Hashtable attrs = new Hashtable(); 277 attrs.put("program", cmdline[0]); 278 attrs.put("returnVal", String.valueOf(processReturnVal)); 279 280 StringBuffer buf = new StringBuffer(); 281 if ((null != errBuf) && (errBuf.length() > 0)) 282 { 283 buf.append("<system-err>"); 284 buf.append(errBuf); 285 buf.append("</system-err>\n"); 286 } 287 if ((null != outBuf) && (outBuf.length() > 0)) 288 { 289 buf.append("<system-out>"); 290 buf.append(outBuf); 291 buf.append("</system-out>\n"); 292 } 293 logger.logElement(Logger.INFOMSG, "checkOutputStreams", attrs, buf.toString()); 294 buf = null; 295 296 // Also log out a perf element by default 297 attrs = new Hashtable(); 298 attrs.put("program", cmdline[0]); 299 attrs.put("isExternal", "true"); 300 attrs.put("timeExec", new Long(timeExec)); 301 logPerf(datalet, attrs); 302 attrs = null; 303 304 // Also call worker method to allow subclasses to 305 // override checking of the output streams, as available 306 checkStreams(datalet, cmdline, outBuf, errBuf, processReturnVal); 307 } 308 309 310 /** 311 * Worker method to validate the System.out/.err streams. 312 * 313 * Default implementation does nothing; override if you wish 314 * to actually validate the specific streams. 315 * 316 * @param datalet that defined the test data 317 * @param cmdline that was used for execProcess 318 * @param outBuf buffer from execProcess' System.out 319 * @param errBuf buffer from execProcess' System.err 320 * @param processReturnVal from execProcess 321 */ checkStreams(FileDatalet datalet, String[] cmdline, StringBuffer outBuf, StringBuffer errBuf, int processReturnVal)322 protected void checkStreams(FileDatalet datalet, String[] cmdline, 323 StringBuffer outBuf, StringBuffer errBuf, int processReturnVal) 324 { 325 // Default impl is no-op 326 return; 327 } 328 329 330 /** 331 * Worker method to write performance data in standard format. 332 * 333 * Writes out a perf elem with standardized idref, testlet, 334 * input/output, and fileSize params. 335 * 336 * @param datalet to use for idref, etc. 337 * @param hash of extra attributes to log. 338 */ logPerf(FileDatalet datalet, Hashtable hash)339 protected void logPerf(FileDatalet datalet, Hashtable hash) 340 { 341 if (null == hash) 342 hash = new Hashtable(); 343 344 File f = new File(datalet.getInput()); 345 346 hash.put("idref", f.getName()); 347 hash.put("input", datalet.getInput()); 348 hash.put("output", datalet.getOutput()); 349 hash.put("testlet", thisClassName); 350 try 351 { 352 // Attempt to store size of input file, since overall 353 // amount of data affects performance 354 hash.put("fileSize", new Long(f.length())); 355 } 356 catch (Exception e) 357 { 358 hash.put("fileSize", "threw: " + e.toString()); 359 } 360 361 logger.logElement(Logger.STATUSMSG, "perf", hash, getCheckDescription(datalet)); 362 } 363 364 } // end of class ExecTestlet 365 366