001: /* Copyright (c) 2001-2005, The HSQL Development Group
002: * All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * Redistributions of source code must retain the above copyright notice, this
008: * list of conditions and the following disclaimer.
009: *
010: * Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * Neither the name of the HSQL Development Group nor the names of its
015: * contributors may be used to endorse or promote products derived from this
016: * software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
020: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
021: * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
022: * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
026: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package org.hsqldb.test;
032:
033: import java.io.BufferedInputStream;
034: import java.io.File;
035: import java.io.FileInputStream;
036: import java.io.FileNotFoundException;
037: import java.io.FileOutputStream;
038: import java.io.IOException;
039: import java.io.InputStream;
040: import java.io.OutputStream;
041: import java.util.List;
042:
043: // $Id: ExecHarness.java,v 1.10 2005/10/23 19:25:13 fredt Exp $
044:
045: /**
046: * Utilities that test classes can call to execute a specified command and to
047: * evaluate the exit status and output of said execution.
048: * harnessInstance.exec() executes the given program (Java or not).
049: * Any time thereafter, harnessInstance can be interrogated for exit
050: * status and text output.
051: *
052: * ExecHarness can emulate user interaction with SqlTool, but you can
053: * not use ExecHarness interactively.
054: *
055: * To execute java classes, you can either give the classpath by setting the
056: * environmental var before running this program, or by giving the classpath
057: * switch to the target program. Classpath switches used for invoking
058: * this ExecHarness class WILL NOT EFFECT java executions by ExecHarness.
059: * E.g. the java invocation
060: * "java org.hsqldb.test.ExecHarness java -cp newcp Cname" will give Cname
061: * classpath of 'newcp', but the following WILL NOT:
062: * "java -cp newcp org.hsqldb.test.ExecHarness java Cname".
063: * It's often easier to just set (and export if necessary) CLASSPATH before
064: * invoking ExecHarness.
065: *
066: * Same applies to java System Properties. You must supply them after the
067: * 2nd "java".
068: *
069: * @see main() for an example of use.
070: */
071: public class ExecHarness {
072:
073: /*
074: * In general, for output from the program, we use Strings so that we can
075: * use regexes with them.
076: * For my current needs, I just need to be able to supply stdin to the
077: * target program by a file, so that's all I'm implementing for stdin
078: * right now.
079: */
080: private static final String SYNTAX_MSG = "SYNTAX: java org.hsqldb.test.ExecHarness targetprogram [args...]";
081: private static final int MAX_PROG_OUTPUT = 10240;
082:
083: /**
084: * To test the ExecHarness class itself.
085: * (Basically, a sanity check).
086: *
087: * Note that we always exec another process. This makes it safe to
088: * execute Java classes which may call System.exit().
089: *
090: * @param sa sa[0] is the program to be run.
091: * Remaining arguments will be passed as command-line args
092: * to the sa[0] program.
093: */
094: public static void main(String[] sa) throws IOException,
095: FileNotFoundException, InterruptedException {
096:
097: byte[] localBa = new byte[10240];
098:
099: if (sa.length < 1) {
100: System.err.println(SYNTAX_MSG);
101: System.exit(1);
102: }
103:
104: String progname = sa[0];
105:
106: System.err
107: .println("Enter any input that you want passed to SqlTool via stdin\n"
108: + "(end with EOF, like Ctrl-D or Ctrl-Z+ENTER):");
109:
110: File tmpFile = File.createTempFile("ExecHarness-", ".input");
111: String specifiedCharSet = System.getProperty("harness.charset");
112: String charset = ((specifiedCharSet == null) ? DEFAULT_CHARSET
113: : specifiedCharSet);
114: FileOutputStream fos = new FileOutputStream(tmpFile);
115: int i;
116:
117: while ((i = System.in.read(localBa)) > 0) {
118: fos.write(localBa, 0, i);
119: }
120:
121: fos.close();
122:
123: ExecHarness harness = new ExecHarness(progname);
124:
125: harness.setArgs(shift(sa));
126: harness.setInput(tmpFile);
127: harness.exec();
128: tmpFile.delete();
129:
130: int retval = harness.getExitValue();
131:
132: System.err
133: .println("STDOUT ******************************************");
134: System.out.print(harness.getStdout());
135: System.err
136: .println("ERROUT ******************************************");
137: System.err.print(harness.getErrout());
138: System.err
139: .println("*************************************************");
140: System.err.println(progname + " exited with value " + retval);
141: harness.clear();
142: System.exit(retval);
143: }
144:
145: File input = null;
146: String program = null;
147: int exitValue = 0;
148: boolean executed = false;
149:
150: // I'm sure there's a better way to do this. Can't think of it right now.
151: String[] mtStringArray = {};
152: String[] args = mtStringArray;
153:
154: // The extra 1 is so we can request 1 more byte than we want.
155: // If that read is satisfied, we know that we read > MAX_PROG_OUTPUT.
156: private byte[] ba = new byte[MAX_PROG_OUTPUT + 1];
157: private String stdout = null;
158: private String errout = null;
159: private static final String DEFAULT_CHARSET = "US-ASCII";
160:
161: /*
162: * Execute associated program synchronously, but in a separate process.
163: * Would be easy to run it asynchronously, but I think this is more
164: * useful as-is for unit testing purposes.
165: * To run a Java class, give args like
166: * <PRE>{ "java", "org.hsqldb.util.SqlTool", "mem" }</PRE>
167: *
168: * Intentionally passes through many exceptions so that the unit testing
169: * tool may properly process these are errors of the testing procedure,
170: * not a failed test.
171: *
172: * In addition to passed-through exceptions, this method will throw
173: * an IOException if the invoked program generates > 10 k of output
174: * to either stdout or errout.
175: */
176: public void exec() throws IOException, InterruptedException {
177:
178: InputStream stream;
179: int i;
180: int writePointer;
181:
182: if (executed) {
183: throw new IllegalStateException(
184: "You have already executed '" + program
185: + "'. Run clear().");
186: }
187:
188: Process proc = Runtime.getRuntime()
189: .exec(unshift(program, args));
190: OutputStream outputStream = proc.getOutputStream();
191:
192: if (input != null) {
193: BufferedInputStream bis = new BufferedInputStream(
194: new FileInputStream(input));
195:
196: while ((i = bis.read(ba)) > 0) {
197: outputStream.write(ba, 0, i);
198: }
199: }
200:
201: outputStream.close();
202:
203: stream = proc.getInputStream();
204: writePointer = 0;
205:
206: while ((i = stream.read(ba, writePointer, ba.length
207: - writePointer)) > 0) {
208: writePointer += i;
209: }
210:
211: if (i > -1) {
212: throw new IOException(program + " generated > "
213: + (ba.length - 1) + " bytes of standard output");
214: }
215:
216: stream.close();
217:
218: executed = true; // At this point we are changing state. No going back.
219: stdout = new String(ba, 0, writePointer);
220: stream = proc.getErrorStream();
221: writePointer = 0;
222:
223: while ((i = stream.read(ba, writePointer, ba.length
224: - writePointer)) > 0) {
225: writePointer += i;
226: }
227:
228: if (i > -1) {
229: throw new IOException(program + " generated > "
230: + (ba.length - 1) + " bytes of error output");
231: }
232:
233: stream.close();
234:
235: errout = new String(ba, 0, writePointer);
236: exitValue = proc.waitFor();
237: }
238:
239: /*
240: * You must run this method before preparing an ExecHarness for re-use
241: * (I.e. to exec() again).
242: */
243: public void clear() {
244:
245: // TODO: Release output buffers.
246: args = mtStringArray;
247: executed = false;
248: stdout = errout = null;
249: input = null;
250: }
251:
252: public String getStdout() {
253: return stdout;
254: }
255:
256: public String getErrout() {
257: return errout;
258: }
259:
260: /**
261: * @param inFile There is no size limit on the input file.
262: */
263: public void setInput(File inFile) throws IllegalStateException {
264:
265: if (executed) {
266: throw new IllegalStateException(
267: "You have already executed '" + program
268: + "'. Run clear().");
269: }
270:
271: input = inFile;
272: }
273:
274: public void setArgs(String[] inArgs) throws IllegalStateException {
275:
276: if (executed) {
277: throw new IllegalStateException(
278: "You have already executed '" + program
279: + "'. Run clear().");
280: }
281:
282: args = inArgs;
283: }
284:
285: public void setArgs(List list) throws IllegalStateException {
286: setArgs(listToPrimitiveArray(list));
287: }
288:
289: int getExitValue() throws IllegalStateException {
290:
291: if (!executed) {
292: throw new IllegalStateException("You have not executed '"
293: + program + "' yet");
294: }
295:
296: return exitValue;
297: }
298:
299: /**
300: * Create an ExecHarness instance which can invoke the given program.
301: *
302: * @param inName Name of the external program (like "cat" or "java").
303: */
304: public ExecHarness(String inName) {
305: program = inName;
306: }
307:
308: /**
309: * These utility methods really belong in a class in the util package.
310: */
311: public static String[] unshift(String newHead, String[] saIn) {
312:
313: String[] saOut = new String[saIn.length + 1];
314:
315: saOut[0] = newHead;
316:
317: for (int i = 1; i < saOut.length; i++) {
318: saOut[i] = saIn[i - 1];
319: }
320:
321: return saOut;
322: }
323:
324: public static String[] shift(String[] saIn) {
325:
326: String[] saOut = new String[saIn.length - 1];
327:
328: for (int i = 0; i < saOut.length; i++) {
329: saOut[i] = saIn[i + 1];
330: }
331:
332: return saOut;
333: }
334:
335: public static String[] listToPrimitiveArray(List list) {
336:
337: String[] saOut = new String[list.size()];
338:
339: for (int i = 0; i < list.size(); i++) {
340: saOut[i] = (String) list.get(i);
341: }
342:
343: return saOut;
344: }
345:
346: public static String[] push(String newTail, String[] saIn) {
347:
348: String[] saOut = new String[saIn.length + 1];
349:
350: for (int i = 0; i < saIn.length; i++) {
351: saOut[i] = saIn[i];
352: }
353:
354: saOut[saOut.length - 1] = newTail;
355:
356: return saOut;
357: }
358:
359: public static String[] pop(String[] saIn) {
360:
361: String[] saOut = new String[saIn.length - 1];
362:
363: for (int i = 0; i < saOut.length; i++) {
364: saOut[i] = saIn[i];
365: }
366:
367: return saOut;
368: }
369:
370: public static String stringArrayToString(String[] sa) {
371:
372: StringBuffer sb = new StringBuffer("{");
373:
374: for (int i = 0; i < sa.length; i++) {
375: if (i > 0) {
376: sb.append(',');
377: }
378:
379: sb.append(sa[i]);
380: }
381:
382: return sb.toString() + '}';
383: }
384: }
|