1 /*
   2  * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx;
  27 
  28 import com.sun.javafx.stage.StageHelper;
  29 import javafx.animation.AnimationTimer;
  30 import javafx.application.Application;
  31 import javafx.application.Platform;
  32 import javafx.stage.Stage;
  33 import junit.framework.Assert;
  34 import org.junit.internal.runners.statements.Fail;
  35 import org.junit.internal.runners.statements.InvokeMethod;
  36 import org.junit.rules.MethodRule;
  37 import org.junit.runners.model.FrameworkMethod;
  38 import org.junit.runners.model.Statement;
  39 
  40 import java.lang.ref.Reference;
  41 import java.lang.reflect.Method;
  42 import java.util.ArrayList;
  43 import java.util.Arrays;
  44 import java.util.List;
  45 import java.util.Map;
  46 import java.util.concurrent.CountDownLatch;
  47 import java.util.concurrent.atomic.AtomicReference;
  48 import java.util.function.Function;
  49 import java.util.stream.Collector;
  50 import java.util.stream.Collectors;
  51 import java.util.stream.Stream;
  52 
  53 public class FXUnit implements MethodRule {
  54 
  55     private static CountDownLatch startupLatch = new CountDownLatch(1);
  56 
  57     public static class TestApplication extends Application {
  58         public TestApplication() { }
  59         @Override
  60         public void start(Stage primaryStage) throws Exception {
  61             Platform.setImplicitExit(false);
  62             primaryStage.close();
  63             startupLatch.countDown();
  64         }
  65     }
  66 
  67     public static class AsyncException extends RuntimeException {
  68         private AsyncException(Exception e) {
  69             super();
  70             initCause(e);
  71         }
  72     }
  73 
  74     private class InvokeAndWaitStatement extends Statement {
  75         private Statement statement;
  76         private InvokeAndWaitStatement(Statement statement) {
  77             this.statement = statement;
  78         }
  79 
  80         @Override
  81         public void evaluate() throws Throwable {
  82             invokeAndWait(statement);
  83         }
  84     }
  85 
  86     public static interface TestRunnable {
  87         public void run() throws Exception;
  88     }
  89 
  90     public FXUnit() {
  91         synchronized (FXUnit.class) {
  92             startToolkit();
  93         }
  94     }
  95 
  96 
  97     @Override
  98     public Statement apply(Statement statement, FrameworkMethod frameworkMethod,
  99                            Object test) {
 100         List<Statement> befores = new ArrayList<>();
 101         List<Statement> afters = new ArrayList<>();
 102         try {
 103             Method[] methods = test.getClass().getMethods();
 104             for (Method m : methods) {
 105                 FXBefore before = m.getAnnotation(FXBefore.class);
 106                 if (before != null) {
 107                     InvokeMethod invoker = new InvokeMethod(
 108                             new FrameworkMethod(m), test);
 109                     if (before.value().equals(FXThread.MAIN)) {
 110                         befores.add(invoker);
 111                     } else {
 112                         befores.add(new InvokeAndWaitStatement(invoker));
 113                     }
 114                 }
 115                 FXAfter after = m.getAnnotation(FXAfter.class);
 116                 if (after != null) {
 117                     InvokeMethod invoker = new InvokeMethod(
 118                             new FrameworkMethod(m), test);
 119                     if (after.value().equals(FXThread.MAIN)) {
 120                         afters.add(invoker);
 121                     } else {
 122                         afters.add(new InvokeAndWaitStatement(invoker));
 123                     }
 124                 }
 125             }
 126         } catch (Exception e) {
 127             return new Fail(e);
 128         }
 129         FXTestThread threadAnnotation = frameworkMethod.getAnnotation(FXTestThread.class);
 130         FXThread testThread = threadAnnotation == null
 131                               ? FXThread.APPLICATION
 132                               : threadAnnotation.value();
 133         Statement newStatement = testThread.equals(FXThread.MAIN)
 134                 ? statement
 135                 : new InvokeAndWaitStatement(statement);
 136         return new Statement() {
 137             @Override
 138             public void evaluate() throws Throwable {
 139                 for (Statement s : befores) {
 140                     s.evaluate();
 141                 }
 142                 newStatement.evaluate();
 143                 for (Statement s : afters) {
 144                     s.evaluate();
 145                 }
 146             }
 147         };
 148     }
 149 
 150     public synchronized void startToolkit() {
 151         if (startupLatch.getCount() != 0) {
 152             if (Boolean.getBoolean("headless")) {
 153                 System.getProperty("glass.platform", "Monocle");
 154                 System.getProperty("monocle.platform", "Headless");
 155                 System.getProperty("prism.order", "sw");
 156             }
 157             new Thread(() -> Application.launch(TestApplication.class)).start();
 158             try {
 159                 startupLatch.await();
 160             } catch (InterruptedException e) {
 161                 Assert.fail("Interrupted when initializing toolkit");
 162             }
 163         } else {
 164             invokeAndWait(() -> {
 165                 List<Stage> stages = new ArrayList<>();
 166                 stages.addAll(StageHelper.getStages());
 167                 stages.forEach((stage) -> stage.close());
 168             });
 169             waitForNextPulse();
 170         }
 171     }
 172 
 173     public void waitForNextPulse() {
 174         CountDownLatch pulseLatch = new CountDownLatch(1);
 175         Platform.runLater(() -> new AnimationTimer() {
 176             @Override
 177             public void handle(long now) {
 178                 pulseLatch.countDown();
 179                 stop();
 180             }
 181         }.start());
 182         try {
 183             pulseLatch.await();
 184         } catch (InterruptedException e) {
 185             Assert.fail("Interrupted when waiting for the next pulse");
 186         }
 187     }
 188 
 189     public void invokeAndWait(TestRunnable runnable) {
 190         invokeAndWait(new Statement() {
 191             @Override
 192             public void evaluate() throws Throwable {
 193                 runnable.run();
 194             }
 195         });
 196     }
 197 
 198     private void invokeAndWait(Statement statement) {
 199         if (Platform.isFxApplicationThread()) {
 200             try {
 201                 statement.evaluate();
 202             } catch (RuntimeException | Error e) {
 203                 throw e;
 204             } catch (Throwable t) {
 205                 throw new AsyncException((Exception) t);
 206             }
 207         } else {
 208             final CountDownLatch runLatch = new CountDownLatch(1);
 209             final AtomicReference<Throwable> throwable = new AtomicReference<>();
 210             Platform.runLater(() -> {
 211                 try {
 212                     try {
 213                         statement.evaluate();
 214                     } catch (Throwable t) {
 215                         throwable.set(t);
 216                     }
 217                 } finally {
 218                     runLatch.countDown();
 219                 }
 220             });
 221             try {
 222                 runLatch.await();
 223             } catch (InterruptedException e) {
 224                 throw new AsyncException(e);
 225             }
 226             Throwable t = throwable.get();
 227             if (t != null) {
 228                 if (t instanceof Error) {
 229                     throw (Error) t;
 230                 } else if (t instanceof RuntimeException) {
 231                     throw (RuntimeException) t;
 232                 } else if (t instanceof Exception) {
 233                     throw new AsyncException((Exception) t);
 234                 }
 235             }
 236         }
 237     }
 238 
 239 }
 240