001/* Run the Tcl tests under JUnit.
002
003   Copyright (c) 2011-2018 The Regents of the University of California.
004   All rights reserved.
005   Permission is hereby granted, without written agreement and without
006   license or royalty fees, to use, copy, modify, and distribute this
007   software and its documentation for any purpose, provided that the above
008   copyright notice and the following two paragraphs appear in all copies
009   of this software.
010
011   IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012   FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013   ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014   THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015   SUCH DAMAGE.
016
017   THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020   PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021   CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022   ENHANCEMENTS, OR MODIFICATIONS.
023
024   PT_COPYRIGHT_VERSION_2
025   COPYRIGHTENDKEY
026
027 */
028
029package ptolemy.util.test.junit;
030
031import static org.junit.Assert.assertEquals;
032
033import java.io.File;
034import java.io.FilenameFilter;
035import java.io.IOException;
036import java.lang.reflect.Method;
037import java.util.Arrays;
038import java.util.Comparator;
039import java.util.Locale;
040
041import org.junit.AfterClass;
042import org.junit.Test;
043import org.junit.runner.RunWith;
044
045import junitparams.JUnitParamsRunner;
046import junitparams.Parameters;
047
048///////////////////////////////////////////////////////////////////
049//// TclTests
050/**
051 * Run the Tcl tests under JUnit.
052 *
053 * <p>
054 * This test must be run from the directory that contains the auto/ directory,
055 * for example:
056 * </p>
057 *
058 * <pre>
059 * (cd ~/ptII/ptolemy/actor/lib/io/test; java -classpath ${PTII}:${PTII}/lib/ptjacl.jar:${PTII}/lib/junit-4.8.2.jar:${PTII}/lib/JUnitParams-0.3.0.jar org.junit.runner.JUnitCore ptolemy.util.test.junit.TclTests)
060 * </pre>
061 *
062 * <p>
063 * This test uses JUnitParams from <a
064 * href="http://code.google.com/p/junitparams/#in_browser"
065 * >http://code.google.com/p/junitparams/</a>, which is released under <a
066 * href="http://www.apache.org/licenses/LICENSE-2.0#in_browser">Apache License
067 * 2.0</a>.
068 * </p>
069 *
070 * @author Christopher Brooks
071 * @version $Id$
072 * @since Ptolemy II 10.0
073 * @Pt.ProposedRating Red (cxh)
074 * @Pt.AcceptedRating Red (cxh)
075 */
076@RunWith(JUnitParamsRunner.class)
077public class TclTests {
078
079    /**
080     * Call the Tcl doneTests command to print out the number of errors.
081     *
082     * @exception Throwable
083     *                If the class, constructor or method cannot be found. or if
084     *                the Interp cannot be instantiated.
085     */
086    @AfterClass
087    public static void doneTests() throws Throwable {
088
089        if (_tclFileCount == 0) {
090            // No .tcl files were found, so testDefs.tcl, which
091            // defines the Tcl doneTests command has *not* been
092            // sourced.  So, we return.
093            return;
094        }
095
096        // util/testsuite/testDefs.tcl doneTests tcl command checks
097        // the value of the reallyExit tcl variable.  If reallyExit is
098        // not present or 1, then ::tycho::TopLevel::exitProgram is
099        // called.  We don't want that because it prints an error
100        // message, so we set reallyExit to 0.
101        _setVarMethod.invoke(_interp, new Object[] { "reallyExit",
102                _tclObjectZero, 1 /*TCL.GLOBAL_ONLY*/ });
103
104        // Invoke the doneTests Tcl command which prints the number of
105        // tests.
106        try {
107            _evalMethod.invoke(_interp, new Object[] { "doneTests", 0 });
108        } catch (Throwable throwable) {
109            if (!_tclExceptionClass.isInstance(throwable.getCause())) {
110                throw throwable;
111            } else {
112                Integer completionCode = (Integer) _getCompletionCodeMethod
113                        .invoke(throwable.getCause(), new Object[] {});
114                if (completionCode.intValue() == 1 /** TCL.ERROR */
115                ) {
116                    // The completion code was 1, which means that the
117                    // command could not be completed successfully.
118
119                    // The Tcl errorInfo global variable will have information
120                    // about what went wrong.
121                    Object errorInfoTclObject = _getVarMethod.invoke(_interp,
122                            new Object[] { "errorInfo", null,
123                                    1 /*TCL.GLOBAL_ONLY*/
124                            });
125                    throw new Exception(
126                            "Evaluating the Tcl method \"doneTests\" "
127                                    + "resulted in a TclException being thrown.\nThe Tcl "
128                                    + "errorInfo global variable has the value:\n"
129                                    + errorInfoTclObject);
130                }
131            }
132        }
133    }
134
135    /**
136     * Return a two dimensional array of arrays of strings that name the .tcl files
137     * to be executed. If there are no .tcl files, return a list with one element that
138     * has the value of the {@link #THERE_ARE_NO_TCL_TESTS} field.
139     *
140     * @return The List of tcl tests.
141     * @exception IOException If there is a problem accessing the auto/ directory.
142     */
143    public Object[] parametersForRunTclFile() throws IOException {
144        String[] tclFiles = new File(".").list(new FilenameFilter() {
145            /**
146             * Return true if the file name ends with .tcl and is not
147             * alljtests.tcl or testDefs.tcl
148             *
149             * @param directory
150             *            Ignored
151             * @param name
152             *            The name of the file.
153             * @return true if the file name ends with .xml or .moml
154             */
155            @Override
156            public boolean accept(File directory, String name) {
157                String fileName = name.toLowerCase(Locale.getDefault());
158                if (fileName.endsWith(".tcl")) {
159                    // alljsimpletests.tcl calls exit,
160                    // which results in JUnit
161                    // producing
162                    // "junit.framework.AssertionFailedError:
163                    // Forked Java VM exited
164                    // abnormally. Please note the
165                    // time in the report does not
166                    // reflect the time until the VM
167                    // exit."
168
169                    if (!fileName.endsWith("alljsimpletests.tcl")
170                            && !fileName.endsWith("alljtests.tcl")
171                            && !fileName.endsWith("testdefs.tcl")) {
172                        return true;
173                    }
174                }
175                return false;
176            }
177        });
178
179        if (tclFiles.length > 0) {
180            int i = 0;
181            Object[][] data = new Object[tclFiles.length][1];
182            for (String tclFile : tclFiles) {
183                data[i++][0] = new File(tclFile).getCanonicalPath();
184            }
185            // Sort the tcl files so that _Configuration.tcl is first
186            // in ptolemy/actor/gui/test
187            // File.list() returns files in a different order
188            // on different platforms.  So much for write once, run everywhere.
189            Arrays.sort(data, new Comparator<Object[]>() {
190                @Override
191                public int compare(final Object[] entry1,
192                        final Object[] entry2) {
193                    final String file1 = (String) entry1[0];
194                    final String file2 = (String) entry2[0];
195                    return file1.compareTo(file2);
196                }
197            });
198            return data;
199        } else {
200            return new Object[][] { { THERE_ARE_NO_TCL_TESTS } };
201        }
202    }
203
204    /**
205     * Run a tclFile.
206
207     * <p>Timeout after 480000 ms.  The
208     * ptolemy/cg/kernel/generic/program/procedural/java/test/AutoAdapter.tcl
209     * test requires more than 240 seconds.</p>
210     *
211     * @param tclFile
212     *            The full path to the .tcl file to be executed. If tclFile
213     *            ends with the value of the {@link #THERE_ARE_NO_TCL_TESTS},
214     *            then the method returns immediately.
215     * @exception Throwable If thrown while executing the tclFile.
216     */
217    @Test
218    @Parameters
219    public void RunTclFile(String tclFile) throws Throwable {
220        if (tclFile.endsWith(THERE_ARE_NO_TCL_TESTS)) {
221            System.out.println(
222                    "No tcl tests in " + System.getProperty("user.dir"));
223            System.out.flush();
224            return;
225        }
226        // Keep track of the number of Tcl files evaluated
227        // If 1 or more files were evaluated, then we call doneTests.
228        TclTests._incrementTclFileCount();
229
230        System.out.println(tclFile);
231        System.out.flush();
232        try {
233            _evalFileMethod.invoke(_interp, new Object[] { tclFile });
234        } catch (Throwable throwable) {
235            if (!_tclExceptionClass.isInstance(throwable.getCause())) {
236                throw throwable;
237            } else {
238                Integer completionCode = (Integer) _getCompletionCodeMethod
239                        .invoke(throwable.getCause(), new Object[] {});
240                if (completionCode.intValue() == 1 /** TCL.ERROR */
241                ) {
242                    // The completion code was 1, which means that the
243                    // command could not be completed successfully.
244
245                    // The Tcl errorInfo global variable will have information
246                    // about what went wrong.
247                    Object errorInfoTclObject = null;
248                    try {
249                        errorInfoTclObject = _getVarMethod.invoke(_interp,
250                                new Object[] { "errorInfo", null,
251                                        1 /*TCL.GLOBAL_ONLY*/
252                                });
253                        throw new Exception("Evaluating the Tcl file \""
254                                + tclFile
255                                + "\"resulted in a TclException being thrown.\nThe Tcl "
256                                + "errorInfo global variable has the value:\n"
257                                + errorInfoTclObject);
258                    } catch (Throwable throwable2) {
259                        throwable2.printStackTrace();
260                        throw new Exception("Evaluating the Tcl file \""
261                                + tclFile
262                                + "\"resulted in a TclException being thrown.\n"
263                                + "The Tcl errorInfo variable could not be obtained.\n"
264                                + throwable2, throwable);
265                    }
266                }
267            }
268        }
269
270        // Get the value of the Tcl FAILED global variable.
271        // We check for non-zero results for *each* .tcl file.
272        Object newFailedCountTclObject = _getVarMethod.invoke(_interp,
273                new Object[] { "FAILED", null, 1 /*TCL.GLOBAL_ONLY*/
274                });
275        int newFailed = Integer.parseInt(newFailedCountTclObject.toString());
276        int lastFailed = Integer.parseInt(_failedTestCount.toString());
277
278        // We only report if the number of test failures has increased.
279        // this prevents us from reporting cascading errors if the
280        // first .tcl file has a failure.
281        TclTests._setFailedTestCount(_newInstanceTclIntegerMethod.invoke(null,
282                new Object[] { Integer.valueOf(newFailed) }));
283
284        // If the Tcl FAILED global variable is not equal to the
285        // previous number of failures, then add a failure.
286        assertEquals("Number of failed tests is has increased.", lastFailed,
287                newFailed);
288    }
289
290    ///////////////////////////////////////////////////////////////////
291    ////                         protected methods                 ////
292
293    /** Increment the count of the number of tcl files visited.
294     * Keep track of the number of Tcl files evaluated
295     * If 1 or more files were evaluated, then we call doneTests.
296     */
297    protected static void _incrementTclFileCount() {
298        // To avoid FindBugs: Write to static field from instance method
299        _tclFileCount++;
300    }
301
302    /** Set the cached value of the count of the number of failed tests.
303     *  @param count The object representing the number of failed tests.
304     */
305    protected static void _setFailedTestCount(Object count) {
306        // To avoid FindBugs: Write to static field from instance method
307        _failedTestCount = count;
308    }
309
310    ///////////////////////////////////////////////////////////////////
311    ////                         protected variables               ////
312
313    /**
314     * A special string that is passed when there are no tcl tests. This is
315     * necessary to avoid an exception in the JUnitParameters.
316     */
317    protected final static String THERE_ARE_NO_TCL_TESTS = "ThereAreNoTclTests";
318
319    ///////////////////////////////////////////////////////////////////
320    ////                         private variables                 ////
321
322    /** The tcl.lang.Interp.eval(String, int) method. */
323    private static Method _evalMethod;
324
325    /** The tcl.lang.Interp.evalFile(String) method. */
326    private static Method _evalFileMethod;
327
328    /** The number of failed tests.  Each .tcl file tests to see if
329     * the number has increased.
330     */
331    private static Object _failedTestCount;
332
333    /** The tcl.lang.TclException.getCompletionCode() method. */
334    private static Method _getCompletionCodeMethod;
335
336    /** The tcl.lang.Interp.getVar(String name1, String name2, int flags) method. */
337    private static Method _getVarMethod;
338
339    /**
340     * The tcl.lang.Interp class. We use reflection here to avoid false
341     * dependencies if auto/ does not exist.
342     */
343    private static Class<?> _interpClass;
344
345    /**
346     * The tcl.lang.Interp object upon which we invoke evalFile(String).
347     */
348    private static Object _interp;
349
350    /** The newInstance() method of the tcl.lang.TclInteger class. */
351    private static Method _newInstanceTclIntegerMethod;
352
353    /** The tcl.lang.Interp.setVar(String name1, String name2, int flags) method. */
354    private static Method _setVarMethod;
355
356    /** The tcl.lang.TclException class. **/
357    private static Class<?> _tclExceptionClass;
358
359    /** Keep track of the number of Tcl files evaluated
360     * If 1 or more files were evaluated, then we call doneTests.
361     */
362    private static int _tclFileCount = 0;
363
364    /**
365     * The tcl.lang.TclObject class. We use reflection here to avoid false
366     * dependencies if auto/ does not exist.
367     */
368    private static Class<?> _tclObjectClass;
369
370    /**
371     * A tcl.lang.TclObject that has the integer value 0.
372     * Used when we call the doneTests Tcl method.
373     */
374    private static Object _tclObjectZero;
375
376    // We place initialization of the _interp in a static block so
377    // that it happens once per directory of tcl files.  The doneTests() method
378    // prints the number of test case failures for us.
379    static {
380        try {
381            // ptolemy.actor.lib.test.NonStrictTest checks isRunningNightlyBuild and
382            // throws an exception if trainingMode is true.
383            System.setProperty("ptolemy.ptII.isRunningNightlyBuild", "true");
384
385            // ptolemy.util.StringUtilities.exit() checks ptolemy.ptII.doNotExit.
386            System.setProperty("ptolemy.ptII.doNotExit", "true");
387
388            _interpClass = Class.forName("tcl.lang.Interp");
389            _interp = _interpClass.newInstance();
390
391            _evalMethod = _interpClass.getMethod("eval",
392                    new Class[] { String.class, Integer.TYPE });
393
394            _evalFileMethod = _interpClass.getMethod("evalFile", String.class);
395
396            _getVarMethod = _interpClass.getMethod("getVar",
397                    new Class[] { String.class, String.class, Integer.TYPE });
398
399            _tclObjectClass = Class.forName("tcl.lang.TclObject");
400            _setVarMethod = _interpClass.getMethod("setVar", new Class[] {
401                    String.class, _tclObjectClass, Integer.TYPE });
402
403            // Create a TclObject with value 0 for use with the doneTests Tcl proc.
404            Class<?> tclIntegerClass = Class.forName("tcl.lang.TclInteger");
405            _newInstanceTclIntegerMethod = tclIntegerClass
406                    .getMethod("newInstance", new Class[] { Integer.TYPE });
407
408            _tclExceptionClass = Class.forName("tcl.lang.TclException");
409
410            _getCompletionCodeMethod = _tclExceptionClass
411                    .getMethod("getCompletionCode", new Class[] {});
412
413            _tclObjectZero = _newInstanceTclIntegerMethod.invoke(null,
414                    new Object[] { Integer.valueOf(0) });
415
416            _failedTestCount = _newInstanceTclIntegerMethod.invoke(null,
417                    new Object[] { Integer.valueOf(0) });
418
419        } catch (Throwable throwable) {
420            // Exceptions sometimes get marked as multiple failures here so
421            // we print the stack to aid debugging.
422            throwable.printStackTrace();
423            throw new RuntimeException(throwable);
424        }
425    }
426
427}