001/* JUnit test the Kieler Layout mechanism.
002
003 Copyright (c) 2011-2017 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.vergil.basic.layout.kieler.test.junit;
030
031import static org.junit.Assert.assertArrayEquals;
032
033import java.io.File;
034import java.util.Iterator;
035
036import javax.swing.SwingUtilities;
037
038import org.junit.AfterClass;
039import org.junit.BeforeClass;
040
041import ptolemy.actor.gui.Configuration;
042import ptolemy.actor.gui.ConfigurationApplication;
043import ptolemy.actor.gui.Tableau;
044import ptolemy.kernel.CompositeEntity;
045import ptolemy.kernel.util.NamedObj;
046import ptolemy.util.Diff;
047import ptolemy.util.FileUtilities;
048import ptolemy.util.StringUtilities;
049import ptolemy.vergil.basic.BasicGraphFrame;
050import ptolemy.vergil.basic.PtolemyLayoutAction;
051import ptolemy.vergil.basic.layout.kieler.KielerLayoutAction;
052
053///////////////////////////////////////////////////////////////////
054//// KielerJUnitTest
055/**
056 * Test out Kieler by open models, using Kieler to layout the graph
057 * and then doing undo and redo.
058 *
059 * <p>There are two types of tests.</p>
060
061 * <p>1. {@link #_layoutModelCompareAgainstFile(NamedObj, String)} We
062 * read in a model and run the Kieler layout algorithm on on the model
063 * and compare the results against the original model.  This test is
064 * run on regression tests to be sure that the Kieler algorithm has
065 * not changed.  Typically, the models are in the
066 * ptolemy/vergil/basic/layout/kieler/test/junit/models
067 * subdirectory.</p>
068 *
069 * <p>2. We read in a model, use the Ptolemy layouter and then the
070 * Kieler layout algorithm.  We then do an undo, a redo and an undo
071 * and compare.  the model against the model after the Ptolemy
072 * layouter.  This test is used run on models in the Ptolemy tree to
073 * make sure that the Kieler layouter and the undo/redo mechanism
074 * works.</p>
075 *
076 * @author Christopher Brooks
077 * @version $Id$
078 * @since Ptolemy II 10.0
079 * @Pt.ProposedRating Red (cxh)
080 * @Pt.AcceptedRating Red (cxh)
081 */
082public class KielerJUnitTest {
083
084    /** Test the Kieler layout facility.
085     *
086     *  <p>To run, use:</p>
087     *
088     *  <pre>
089     *   java -classpath \
090     *      $PTII:$PTII/lib/junit-4.8.2.jar:$PTII/lib/kieler.jar \
091     *      ptolemy.vergil.basic.layout.kieler.test.junit.KielerJUnitTest
092     *  </pre>
093     *
094     *  @param args Not used.
095     */
096    public static void main(String args[]) {
097        org.junit.runner.JUnitCore.main(
098                "ptolemy.vergil.basic.layout.kieler.test.junit.KielerJUnitTest");
099    }
100
101    /** Save the previous value of the ptolemy.ptII.doNotExit property
102     * and set it to true while running this test.
103     *
104     * @exception Exception If there is a problem opening the model
105     */
106    @BeforeClass
107    public static void openSentinelModel() throws Exception {
108        // Avoid calling System.exit().
109        _previousPropertyValue = StringUtilities
110                .getProperty("ptolemy.ptII.doNotExit");
111        System.setProperty("ptolemy.ptII.doNotExit", "true");
112    }
113
114    // FIXME the following two tests are deactivated because they fail
115    // due to the WindowPropertiesAttribute being different between the
116    // saved (to disk) and layouted model and the freshly layouted model.
117
118    // class="ptolemy.actor.gui.WindowPropertiesAttribute" value="{bounds={465, 154, 833, 619},
119
120    /**
121     * Test the layout facility by reading in a models, stripping
122     * out the graphical elements, laying out the models, comparing
123     * the new results with the known good results and then doing
124     * undo and redo.
125     * @exception Exception If there is a problem reading or laying
126     * out a model.
127     */
128    //    @org.junit.Test
129    //    public void runConstDisplay() throws Exception {
130    //        _layoutTest(
131    //                "$CLASSPATH/ptolemy/vergil/basic/layout/kieler/test/junit/models/ConstDisplay.xml",
132    //                true);
133    //    }
134
135    /** Test the layout of the ConstConstDisplay model.
136     * @exception Exception If there is a problem reading or laying
137     * out a model.
138     */
139    //    @org.junit.Test
140    //    public void runConstConstDisplay() throws Exception {
141    //        _layoutTest(
142    //                "$CLASSPATH/ptolemy/vergil/basic/layout/kieler/test/junit/models/ConstConstDisplay.xml",
143    //                true);
144    //    }
145
146    /* ----------------------------
147     *          Actor Tests
148     * ---------------------------- */
149
150    /** Test the layout of the modulation model.
151     * @exception Exception If there is a problem reading or laying
152     * out a model.
153     */
154    @org.junit.Test
155    public void runModulation() throws Exception {
156        _layoutTest("$CLASSPATH/ptolemy/moml/demo/modulation.xml", false);
157    }
158
159    /** Reset the ptolemy.ptII.doNotExit property to the previous value.
160     * @exception Throwable If there is a problem setting the property.
161     */
162    @AfterClass
163    public static void closeSentinelModel() throws Throwable {
164        System.setProperty("ptolemy.ptII.doNotExit", _previousPropertyValue);
165    }
166
167    ///////////////////////////////////////////////////////////////////
168    ////                         protected methods                 ////
169
170    /**
171     * Test the layout facility by reading in a model, laying out the
172     * model, comparing the new results with the known good results
173     * and then doing undo and redo.
174     *
175     * <p>This is the main entry point for Kieler layout tests.</p>
176     *
177     * <p>The caller of this method need <b>not</b>be in the Swing
178     * Event Thread.</p>
179     *
180     * @param modelFileName The file name of the test model.
181     * @param compareAgainstOriginal  If true, then run the Kieler
182     * Layouter and compare against the original file.  If false, run
183     * the Ptolemy layouter, the Kieler layouter, then undo, redo,
184     * undo and compare against the output after the Ptolemy layouter.
185     * @exception Exception If the file name cannot be read or laid out.
186     */
187    protected void _layoutTest(final String modelFileName,
188            final boolean compareAgainstOriginal) throws Exception {
189
190        // FIXME: this seem wrong:  The inner classes are in different
191        // threads and can only access final variables.  However, we
192        // use an array as a final variable, but we change the value
193        // of the element of the array.  Is this thread safe?
194        final CompositeEntity[] model = new CompositeEntity[1];
195
196        final Throwable[] throwable = new Throwable[1];
197        throwable[0] = null;
198
199        // The basic structure of this method is that we call
200        // invokeAndWait() on operations that display graphics and
201        // then sleep this thread.  This gives us a way to see the
202        // model be laid out.
203
204        /////
205        // Open the model.
206        Runnable openModelAction = new Runnable() {
207            @Override
208            public void run() {
209                try {
210                    System.out.print(" " + modelFileName + " ");
211                    model[0] = ConfigurationApplication
212                            .openModelOrEntity(modelFileName);
213                } catch (Throwable throwableCause) {
214                    throwable[0] = throwableCause;
215                    throw new RuntimeException(throwableCause);
216                }
217            }
218        };
219        SwingUtilities.invokeAndWait(openModelAction);
220        if (throwable[0] != null || model[0] == null) {
221            throw new Exception(
222                    "Failed to open " + modelFileName + throwable[0]);
223        }
224        String baseMoML = model[0].exportMoML();
225        _basicGraphFrame = _getBasicGraphFrame(model[0]);
226
227        _sleep();
228
229        /////
230        // Layout the model using either the Kieler layout mechanism
231        // or the krufty Ptolemy Layout mechanism.
232        // Note that this call is quite important to "normalize" old
233        // models. In older models the location was stored with an expression
234        // like value="0, 0", while in newer models curly braces are used
235        // value="{0, 0}".
236        // If this layout call is not performed, one would have to export
237        // the model and load it again to get a normalized version
238        Runnable layoutModelAction = new Runnable() {
239            @Override
240            public void run() {
241                try {
242                    if (compareAgainstOriginal) {
243                        _layoutModelCompareAgainstFile(model[0], modelFileName);
244                    } else {
245                        // Invoke the crufty Ptolemy layout mechanism and export.
246                        new PtolemyLayoutAction().doAction(model[0]);
247                        _basicGraphFrame.report("Ptolemy Layout done");
248                    }
249                } catch (Throwable throwableCause) {
250                    throwable[0] = throwableCause;
251                    throw new RuntimeException(throwableCause);
252                }
253            }
254        };
255        SwingUtilities.invokeAndWait(layoutModelAction);
256        _sleep();
257        if (throwable[0] != null || model[0] == null) {
258            throw new Exception(
259                    "Failed to layout " + modelFileName + throwable[0]);
260        }
261
262        /////
263        // Optionally invoke the Ptolemy layout mechanism.
264        if (!compareAgainstOriginal) {
265            // We just laid out the model with Ptolemy, now sleep so
266            // that we see the Ptolemy layout and then layout the
267            // model with Kieler.  Don't do a comparison yet.
268            _sleep();
269            // The "original" model is now the model laid out with
270            // the Ptoelmy mechanism.
271            baseMoML = model[0].exportMoML();
272            Runnable kielerLayoutModelAction = new Runnable() {
273                @Override
274                public void run() {
275                    try {
276                        // Invoke the Kieler layout mechanism.
277                        new KielerLayoutAction().doAction(model[0]);
278                    } catch (Throwable throwableCause) {
279                        throwable[0] = throwableCause;
280                        throw new RuntimeException(throwableCause);
281                    }
282                }
283            };
284            SwingUtilities.invokeAndWait(kielerLayoutModelAction);
285            _sleep();
286            if (throwable[0] != null || model[0] == null) {
287                throw new Exception(
288                        "Failed to layout " + modelFileName + throwable[0]);
289            }
290        }
291
292        /////
293        // Loop through undo and redo
294        String laidOutMoML = model[0].exportMoML();
295        for (int i = 1; i <= 2; i++) {
296            Runnable undoAction = new Runnable() {
297                @Override
298                public void run() {
299                    try {
300                        _undo(model[0]);
301                    } catch (Throwable throwableCause) {
302                        throwable[0] = throwableCause;
303                        throw new RuntimeException(throwableCause);
304                    }
305                }
306            };
307            SwingUtilities.invokeAndWait(undoAction);
308            _sleep();
309            if (throwable[0] != null || model[0] == null) {
310                throw new Exception(
311                        "Failed to undo " + modelFileName + throwable[0]);
312            }
313
314            String undoMoML = model[0].exportMoML();
315            if (_debug || !baseMoML.equals(undoMoML)) {
316                System.out.println("Difference between original MoML"
317                        + " and the exported MoML after Kieler Layout and then undo:");
318                System.out.println(Diff.diff(baseMoML, undoMoML));
319            }
320
321            assertArrayEquals(baseMoML.getBytes(), undoMoML.getBytes());
322
323            Runnable redoAction = new Runnable() {
324                @Override
325                public void run() {
326                    try {
327                        _redo(model[0]);
328                    } catch (Throwable throwableCause) {
329                        throwable[0] = throwableCause;
330                        throw new RuntimeException(throwableCause);
331                    }
332                }
333            };
334            SwingUtilities.invokeAndWait(redoAction);
335            String redoMoML = model[0].exportMoML();
336            if (_debug || !laidOutMoML.equals(redoMoML)) {
337                System.out.println("Difference between laid out MoML"
338                        + " and the exported MoML after Kieler Layout and then undo, then redo:");
339                System.out.println(Diff.diff(laidOutMoML, redoMoML));
340            }
341
342            assertArrayEquals(laidOutMoML.getBytes(), redoMoML.getBytes());
343            _sleep();
344            if (throwable[0] != null || model[0] == null) {
345                throw new Exception(
346                        "Failed to redo " + modelFileName + throwable[0]);
347            }
348
349        }
350
351        /////
352        // Close the model.
353        Runnable closeAction = new Runnable() {
354            @Override
355            public void run() {
356                try {
357                    ConfigurationApplication
358                            .closeModelWithoutSavingOrExiting(model[0]);
359                } catch (Throwable throwableCause) {
360                    throwable[0] = throwableCause;
361                    throw new RuntimeException(throwableCause);
362                }
363            }
364        };
365        SwingUtilities.invokeAndWait(closeAction);
366        _sleep();
367        if (throwable[0] != null || model[0] == null) {
368            throw new Exception(
369                    "Failed to close " + modelFileName + throwable[0]);
370        }
371    }
372
373    /** Lay out the model and compare the results against the original
374     *  model file name.
375     *
376     *  <p>The caller of this method should be in the Swing Event
377     *  Thread.</p>
378     *
379     *  @param model The model.
380     *  @param modelFileName The pathname of the model, used for
381     *  comparing.
382     *  @exception Exception If thrown while opening or laying
383     *  out the model.
384     */
385    protected void _layoutModelCompareAgainstFile(NamedObj model,
386            String modelFileName) throws Exception {
387        try {
388            // Invoke the Kieler layout mechanism.
389            new KielerLayoutAction().doAction(model);
390
391            // Export the model and compare it with the original.
392            String laidOutMoML = model.exportMoML();
393            File canonicalModelFile = FileUtilities.nameToFile(modelFileName,
394                    null);
395            String canonicalModelFileName = canonicalModelFile
396                    .getCanonicalPath();
397            byte[] baseMoMLBytes = FileUtilities.binaryReadURLToByteArray(
398                    canonicalModelFile.toURI().toURL());
399
400            if (_debug || !new String(baseMoMLBytes).equals(laidOutMoML)) {
401                System.out.println("Difference between "
402                        + canonicalModelFileName
403                        + " and the exported MoML after Kieler Layout:");
404                System.out.println(
405                        Diff.diff(new String(baseMoMLBytes), laidOutMoML));
406            }
407            assertArrayEquals(laidOutMoML.getBytes(), baseMoMLBytes);
408
409        } catch (Throwable throwable) {
410            throwable.printStackTrace();
411        }
412    }
413
414    /** Redo the last operation on the model.
415     *
416     *  <p>The caller of this method should be in the Swing Event
417     *  Thread.</p>
418     *  @param model The model upon which the last operation
419     *  should be redone.
420     */
421    protected void _redo(NamedObj model) {
422        _basicGraphFrame.report("About to redo");
423        _basicGraphFrame.redo();
424        _basicGraphFrame.report("Redo done");
425    }
426
427    /** Sleep the current thread, which is usually not the Swing Event
428     *  Dispatch Thread.
429     */
430    protected static void _sleep() {
431        try {
432            Thread.sleep(1000);
433        } catch (Throwable ex) {
434            //Ignore
435        }
436    }
437
438    /** Undo the last operation on the model.
439     *
440     *  <p>The caller of this method should be in the Swing Event
441     *  Thread.</p>
442     *  @param model The model upon which the last operation
443     *  should be redone.
444     */
445    protected void _undo(NamedObj model) {
446        _basicGraphFrame.report("About to undo");
447        _basicGraphFrame.undo();
448        _basicGraphFrame.report("Undo done");
449    }
450
451    ///////////////////////////////////////////////////////////////////
452    ////                         private methods                   ////
453
454    private static BasicGraphFrame _getBasicGraphFrame(NamedObj model) {
455        // See PtolemyLayoutAction for similar code.
456        BasicGraphFrame frame = null;
457        Iterator tableaux = Configuration.findEffigy(model)
458                .entityList(Tableau.class).iterator();
459        while (tableaux.hasNext()) {
460            Tableau tableau = (Tableau) tableaux.next();
461            if (tableau.getFrame() instanceof BasicGraphFrame) {
462                frame = (BasicGraphFrame) tableau.getFrame();
463                break;
464            }
465        }
466        // Fetch everything needed to build the LayoutTarget.
467        //GraphController graphController = ((BasicGraphFrame)frame).getJGraph()
468        //    .getGraphPane().getGraphController();
469        //AbstractBasicGraphModel graphModel = (AbstractBasicGraphModel) ((BasicGraphFrame)frame)
470        //    .getJGraph().getGraphPane().getGraphController()
471        //    .getGraphModel();
472        return frame;
473    }
474
475    ///////////////////////////////////////////////////////////////////
476    ////                         private fields                    ////
477
478    /** The BasicGraphFrame of the model. */
479    private BasicGraphFrame _basicGraphFrame;
480
481    /** Set to true for debugging messages. */
482    private final boolean _debug = false;
483
484    /** The value of the ptolemy.ptII.doNotExit property before we started.
485     */
486    private static String _previousPropertyValue = "";
487
488}