001    /*
002     * Copyright (C) 2000-2001 Iowa State University
003     *
004     * This file is part of mjc, the MultiJava Compiler, adapted for ESC/Java2.
005     *
006     * This program is free software; you can redistribute it and/or modify
007     * it under the terms of the GNU General Public License as published by
008     * the Free Software Foundation; either version 2 of the License, or
009     * (at your option) any later version.
010     *
011     * This program is distributed in the hope that it will be useful,
012     * but WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014     * GNU General Public License for more details.
015     *
016     * You should have received a copy of the GNU General Public License
017     * along with this program; if not, write to the Free Software
018     * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019     *
020     * $Id: Utils.java,v 1.10 2005/01/15 03:21:43 cok Exp $
021     * Author: David R. Cok
022     */
023    
024    package junitutils;
025    import java.io.PrintStream;
026    import java.io.*;
027    import java.lang.reflect.Method;
028    
029    /** This class contains miscellaneous (static) utility functions that are
030     useful in writing JUnit functional tests.
031     */
032    public class Utils {
033      
034      /** Setting this field to true disables the capturing of the output;
035       one would do this only for debugging purposes.
036       */
037      static public boolean disable = false;
038      
039      /** A cached value of the usual System out stream. */
040      //@ non_null spec_public
041      final private static PrintStream pso = System.out;
042      
043      /** A cached value of the usual System err stream. */
044      //@ non_null spec_public
045      final private static PrintStream pse = System.err;
046      
047      /** Redirects System.out and System.err to the given PrintStream. 
048       Note that setStreams/restoreStreams operate on the global
049       values of System.out and System.err; these implementations
050       are not synchronized - you will need to take care of any
051       race conditions if you utilize these in more than one thread.
052       
053       @param ps The stream that is the new output and error stream
054       */
055      //@ requires ps != null;
056      //@ ensures System.out == ps && System.err == ps;
057      static public void setStreams(PrintStream ps) {
058        if (disable) return;
059        pso.flush();
060        pse.flush();
061        System.setOut(ps);
062        System.setErr(ps);
063      }
064      
065      /** Creates a new output stream (which is returned) and makes it
066       the stream into which the standard and error outputs are captured.
067       
068       @return an output stream into which standard and error output 
069       is captured
070       */
071      //@ ensures \result != null;
072      static public ByteArrayOutputStream setStreams() {
073        ByteArrayOutputStream ba = new ByteArrayOutputStream(10000);
074        PrintStream ps = new PrintStream(ba);
075        setStreams(ps);
076        return ba;
077      }
078      // TODO - note the hard-coded size of the stream above.  It needs to be
079      // variable, or at least be sure to capture overflows.  The size needs to
080      // be large enough to hold the output of a test.
081      
082      /** Restores System.out and System.err to the initial, 
083       system-defined values. It is ok to call this method
084       even if setStreams has not been called.
085       <p>
086       Note that setStreams/restoreStreams operate on the global
087       values of System.out and System.err; these implementations
088       are not synchronized - you will need to take care of any
089       race conditions if you utilize these in more than one thread.
090       */
091      //@ ensures System.out == pso && System.err == pse;
092      static public void restoreStreams() {
093        restoreStreams(false);
094      }
095      
096      /** Restores System.out and System.err to the initial, 
097       system-defined values. It is ok to call this method
098       even if setStreams has not been called.
099       <p>
100       Note that setStreams/restoreStreams operate on the global
101       values of System.out and System.err; these implementations
102       are not synchronized - you will need to take care of any
103       race conditions if you utilize these in more than one thread.
104       
105       * @param close if true, the current output and error streams
106       *   are closed before being reset (if they are not currently the
107       *   System output and error streams)
108       */
109      static public void restoreStreams(boolean close) {
110        if (disable) return;
111        if (close) {
112          if (pso != System.out) pso.close();
113          if (pse != System.err) pse.close();
114        }
115        System.setOut(pso);
116        System.setErr(pse);
117      }
118      
119      
120      
121      /** Parses a string into arguments as if it were a command-line, using
122       the QuoteTokenizer to parse the tokens.
123       
124       @param s The String to parse
125       @return The input string parsed into command-line arguments
126       */
127      //@ requires s != null;
128      //@ ensures \result != null;
129      //@ ensures !\nonnullelements(\result);
130      static public String[] parseLine(String s) {
131        QuoteTokenizer q = new QuoteTokenizer(s);
132        java.util.ArrayList args = new java.util.ArrayList();
133        while (q.hasMoreTokens()) {
134          String a = q.nextToken();
135          args.add(a);
136        }
137        return (String[])args.toArray(new String[args.size()]);
138      }
139      
140      /** An enumerator that parses a string into tokens, according to the
141       rules a command-line would use.  White space separates tokens,
142       with double-quoted and single-quoted strings recognized.
143       */
144      // FIXME - does not handle escape sequences in strings, nor 
145      // quoted strings that are not the whole token, e.g.
146      // a b c+"asd"  should be three tokens, "a", "b", and  c+"asd" .
147      static public class QuoteTokenizer {
148        /** The String being tokenized */
149        /*@ non_null spec_public */ final private String ss;
150        
151        /** A char array representation of the String being tokenized */
152        /*@ non_null spec_public */ final private char[] cc;
153        
154        /** The position in the char array */
155        /*@ spec_public */
156        private int pos = 0;        /*@ invariant pos >= 0;
157                                        invariant pos <= cc.length;
158                                     */
159        
160        /** Initializes the tokenizer with the given String
161         * @param s the String to be tokenized
162         */
163        //@ requires s != null;
164        //@ modifies ss,cc;
165        //@ ensures s == ss;
166        public QuoteTokenizer(String s) {
167          ss = s;
168          cc = s.toCharArray();
169        }
170        
171        /*@ 
172         ensures \result ==> (pos < cc.length);
173         ensures \result == !(\forall int i; pos<=i && i < cc.length;
174                                       Character.isWhitespace(cc[i]));
175         model public pure boolean moreTokens();
176         */
177        
178        /**
179         * @return true if there are more tokens to be returned
180         */
181        //@ modifies pos;
182        //@ ensures \result == \old(moreTokens());
183        //@ ensures \result ==> !Character.isWhitespace(cc[pos]);
184        //@ ensures moreTokens() == \old(moreTokens());
185        public boolean hasMoreTokens() {
186          while (pos < cc.length && Character.isWhitespace(cc[pos])) ++pos;
187          return pos < cc.length;
188        }
189    
190        /**
191         * @return the next token if there is one, otherwise null
192         */
193        //@ public normal_behavior
194        //@         requires moreTokens();
195        //@   modifies pos;
196        //@   ensures \result != null;
197        //@ also public normal_behavior
198        //@   requires !moreTokens();
199        //@   modifies pos;
200        //@   ensures \result == null;
201        public String nextToken() {
202          String res = null;
203          while (pos < cc.length && Character.isWhitespace(cc[pos])) ++pos;
204          if (pos == cc.length) return res;
205          int start = pos;
206          if (cc[pos] == '"') {
207            ++pos;
208            while (pos < cc.length && cc[pos] != '"' ) ++pos;
209            if (cc[pos] == '"') ++pos;
210            res = ss.substring(start+1,pos-1);
211          } else if (cc[pos] == '\'') {
212            ++pos;
213            while (pos < cc.length && cc[pos] != '\'' ) ++pos;
214            if (cc[pos] == '\'') ++pos;
215            res = ss.substring(start+1,pos-1);
216          } else {
217            while (pos < cc.length && !Character.isWhitespace(cc[pos])) ++pos;
218            res = ss.substring(start,pos);
219          }
220          return res;
221        }
222      }
223      
224      /** Deletes the contents of a directory, including subdirectories.  
225       If the second argument is true, the directory itself is deleted as well.
226       
227       @param d The directory whose contents are deleted
228       @param removeDirectoryItself if true, the directory itself is deleted
229       
230       @return  false if something could not be deleted; 
231       true if everything was successfully deleted
232       */
233      //@ requires d != null;
234      static public boolean recursivelyRemoveDirectory(File d, 
235          boolean removeDirectoryItself) {
236        if (!d.exists()) return true;
237        boolean success = true;
238        File[] fl = d.listFiles();
239        if (fl != null) {
240          for (int ff=0; ff<fl.length; ++ff) {
241            if (fl[ff].isDirectory()) {
242              if (!recursivelyRemoveDirectory(fl[ff],true)) 
243                success = false;
244            } else {
245              if (!fl[ff].delete()) success = false;
246            }
247          }
248        }
249        if (removeDirectoryItself) {
250          if (!d.delete()) success = false;
251        }
252        return success;
253      }
254      
255      /** Reads the contents of the file with the given name, returning a String.
256       This is an optimized version that uses the byte array provided and 
257       presumes that the file is shorter than the length of the array.
258       
259       @param filename              The file to be read
260       @param cb            A temporary byte array to speed reading
261       
262       @return                      The contents of the file
263       @throws java.io.IOException
264       */
265      // FIXME - can we check that the file is too long without losing the efficiency benefits?
266      //@ requires filename != null;
267      //@ requires cb != null;
268      //@ ensures \result != null;
269      static public String readFile(String filename, byte[] cb) 
270      throws java.io.IOException {
271        int i = 0;
272        int j = 0;
273        java.io.FileInputStream f = null;
274        try {
275          f = new java.io.FileInputStream(filename);
276          while (j != -1) {
277            i = i + j;
278            j = f.read(cb,i,cb.length-i);
279          }
280        } finally {
281          if (f != null) f.close();
282        }
283        return new String(cb,0,i);
284      }
285      
286      /**
287       * Reads a file, returning a String containing the contents
288       * @param filename the file to be read
289       * @return the contents of the file as a String, or null if the
290       *      file could not be read
291       */
292      //@ requires filename != null;
293      static public String readFileX(String filename) {
294        try {
295          return readFile(filename);
296        } catch (Exception e ) {
297          System.out.println("Could not read file " + filename);
298          // FIXME - need a better way to report and catch errors
299        }
300        return null;
301      }
302      
303      /** Reads the contents of the file with the given name, returning a String. 
304       This version presumes the file is shorter than an internal buffer.
305       FIXME
306       
307       @param filename              The file to be read
308       @return                              The contents of the file
309       @throws IOException
310       */
311      //@ requires filename != null;
312      //@ ensures \result != null;
313      static public String readFile(String filename) throws java.io.IOException {
314        StringBuffer sb = new StringBuffer(10000);
315        char[] cb = new char[10000];  // This hard-coded value can be anything;
316                    // smaller numbers will be less efficient since more reads
317                    // may result
318        int j = 0;
319        java.io.FileReader f = null;
320        try {
321          f = new java.io.FileReader(filename);
322          while (j != -1) {
323            j = f.read(cb,0,cb.length);
324            if (j != -1) sb.append(cb,0,j);
325          }
326        } finally {
327          if (f != null) f.close();
328        }
329        return sb.toString();
330      }
331      
332      /**
333       * Executes the static compile(String[]) method of the given class
334       * @param cls The class whose 'compile' method is be invoked
335       * @param args   The String[] argument of the method
336       * @return The standard output and error output of the invocation
337       */
338      //@ requires cls != null;
339      //@ requires args != null;
340      //@ requires !\nonnullelements(args);
341      static public String executeCompile(Class cls, String[] args) {
342        return executeMethod(cls,"compile",args);
343      }
344      
345      /** Finds and executes the method with the given name in the given class;
346       the method must have a single argument of type String[].  The 'args'
347       parameter is supplied to it as its argument.
348       * @param cls         The class whose method is to be invoked
349       * @param methodname The method to be invoked
350       * @param args                The argument of the method
351       * @return  The standard output and error output of the invocation
352       */
353      //@ requires cls != null;
354      //@ requires methodname != null;
355      //@ requires args != null;
356      //@ requires !\nonnullelements(args);
357      static public String executeMethod(Class cls, String methodname, String[] args) {
358        try {
359          Method method = cls.getMethod(methodname, new Class[] { String[].class });
360          return executeMethod(method,args);
361        } catch (NoSuchMethodException e) {
362          System.out.println("No method compile in class " + cls);  // FIXME - better error return needed
363          e.printStackTrace();
364          throw new RuntimeException(e.toString());
365        }
366      }
367      
368      /** Calls the given method on the given String[] argument.  Any standard
369       output and error output is collected and returned as the String 
370       return value.
371       * @param method      The static method to be invoked
372       * @param args        The argument of the method
373       * @return            The standard output and error output of the method
374       */
375      //@ requires method != null;
376      //@ requires args != null;
377      //@ requires !\nonnullelements(args);
378      static public String executeMethod(Method method, String[] args) {
379        try {
380          ByteArrayOutputStream ba = new ByteArrayOutputStream(10000); // FIXME - hardcoded size
381          PrintStream ps = new PrintStream(ba);
382          Utils.setStreams(ps);
383          boolean b = ((Boolean)(method.invoke(null,new Object[]{args}))).booleanValue();
384          ps.close(); 
385          String s = ba.toString();
386          return s;
387        } catch (Exception e) {
388          Utils.restoreStreams(); // FIXME - see comment in TestFilesTestSuite.java
389          // Need the above restore before we try to print something
390          System.out.println(e.toString());  // FIXME - need better error handling
391        } finally {
392          Utils.restoreStreams();
393        }
394        return null;
395      }
396      
397      /** The suffix to append to create the golden output filename */
398      static private final String ORACLE_SUFFIX = "-expected";
399      
400      /** The suffix to append to create the actual output filename */
401      static private final String SAVED_SUFFIX = "-ckd";
402      
403      /** Compares the given string to the content of the given file using
404       a comparator that ignores platform differences in line-endings.
405       The method has the side effect of saving the string value in a file
406       for later comparison if the string and file are different, and making
407       sure that the actual output file (the -ckd file) is deleted if the
408       string and file are the same.  The expected output filename is the
409       rootname + "-expected"; the actual output filename is the rootname + "-ckd". 
410       *  
411       * @param s the String to compare
412       * @param rootname the prefix of the file name
413       * @return The Diff structure that contains the comparison
414       * @throws java.io.IOException
415       */
416      static public Diff compareStringToFile(String s, String rootname) 
417                                                   throws java.io.IOException {
418        String ss = Utils.readFile(rootname+ORACLE_SUFFIX);
419        Diff df = new Diff( "expected", ss, "actual", s );
420        
421        if (!df.areDifferent()) {
422          // If the two strings match, the test succeeds and we make sure
423          // that there is no -ckd file to confuse anyone.
424          (new File(rootname+SAVED_SUFFIX)).delete();
425        } else {
426          // If the strings do not match, we save the actual string and 
427          // fail the test.
428          FileWriter f = null;
429          try {
430            f = new FileWriter(rootname+SAVED_SUFFIX);
431            f.write(s); 
432          } finally {
433            if (f != null) f.close(); 
434          }
435        }
436        return df;
437      }
438      
439      /** This deletes all files (in the current directory) whose
440       names match the given pattern in a regular-expression sense;
441       however, it is only implemented for patterns consisting of
442       characters and at most one '*', since I'm not going to rewrite
443       an RE library.
444       * @param pattern the pattern to match against filenames
445       */
446      //@ requires pattern != null;
447      static public void removeFiles(String pattern) {
448        File[] list;
449        int k = pattern.indexOf("*");
450        if (k == -1) {
451          list = new File[] { new File(pattern) };
452        } else if (k == 0) {
453          final String s = pattern.substring(1);
454          FileFilter ff = new FileFilter() { 
455            public boolean accept(File f) { return f.isFile() && f.getName().endsWith(s); }
456          };
457          list = (new File(".")).listFiles(ff);
458        } else if (k+1 == pattern.length()) {
459          final String s = pattern.substring(0,k);
460          FileFilter ff = new FileFilter() { 
461            public boolean accept(File f) { return f.isFile() && f.getName().startsWith(s); }
462          };
463          list = (new File(".")).listFiles(ff);        
464        } else {
465          final String s = pattern.substring(0,k);
466          final String ss = pattern.substring(k+1);
467          final int j = pattern.length()-1;
468          FileFilter ff = new FileFilter() { 
469            public boolean accept(File f) { 
470              return  f.isFile() && 
471              f.getName().length() >= j && 
472              f.getName().startsWith(s) && 
473              f.getName().endsWith(ss); }
474          };
475          list = (new File(".")).listFiles(ff);        
476        }
477        
478        for (int i = 0; i<list.length; ++i) {
479          //System.out.println("DELETING " +list[i].getName());
480          list[i].delete();
481        }
482        
483      }
484    }