1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package vogar.target.junit;
18
19import java.util.List;
20import java.util.concurrent.Callable;
21import java.util.concurrent.ExecutorService;
22import java.util.concurrent.Executors;
23import java.util.concurrent.Future;
24import java.util.concurrent.TimeUnit;
25import java.util.concurrent.TimeoutException;
26import java.util.concurrent.atomic.AtomicReference;
27
28import junit.framework.AssertionFailedError;
29import vogar.Result;
30import vogar.monitor.TargetMonitor;
31import vogar.target.Profiler;
32import vogar.target.Runner;
33import vogar.target.TestEnvironment;
34import vogar.util.Threads;
35
36/**
37 * Adapts a JUnit3 test for use by vogar.
38 */
39public final class JUnitRunner implements Runner {
40
41    private TargetMonitor monitor;
42    private Class<?> testClass;
43    private String qualification;
44    private AtomicReference<String> skipPastReference;
45    private String actionName;
46    private TestEnvironment testEnvironment;
47    private int timeoutSeconds;
48    private boolean vmIsUnstable;
49
50    private final ExecutorService executor = Executors.newCachedThreadPool(
51            Threads.daemonThreadFactory("testrunner"));
52
53    public void init(TargetMonitor monitor, String actionName, String qualification,
54            Class<?> testClass, AtomicReference<String> skipPastReference,
55            TestEnvironment testEnvironment, int timeoutSeconds, boolean profile) {
56        this.monitor = monitor;
57        this.testClass = testClass;
58        this.qualification = qualification;
59        this.skipPastReference = skipPastReference;
60        this.actionName = actionName;
61        this.testEnvironment = testEnvironment;
62        this.timeoutSeconds = timeoutSeconds;
63    }
64
65    public boolean run(String actionName, Profiler profiler, String[] args) {
66        final List<VogarTest> tests;
67        if (Junit3.isJunit3Test(testClass)) {
68            tests = qualification != null
69                    ? Junit3.classToVogarTests(testClass, qualification)
70                    : Junit3.classToVogarTests(testClass, args);
71        } else if (Junit4.isJunit4Test(testClass)) {
72            tests = qualification != null
73                    ? Junit4.classToVogarTests(testClass, qualification)
74                    : Junit4.classToVogarTests(testClass, args);
75        } else {
76            throw new AssertionFailedError("Unknown JUnit type: " + testClass.getName());
77        }
78
79        for (VogarTest test : tests) {
80            String skipPast = skipPastReference.get();
81            if (skipPast != null) {
82                if (skipPast.equals(test.toString())) {
83                    skipPastReference.set(null);
84                }
85                continue;
86            }
87
88            runWithTimeout(profiler, test);
89
90            if (vmIsUnstable) {
91                return false;
92            }
93        }
94
95        return true;
96    }
97
98    /**
99     * Runs the test on another thread. If the test completes before the
100     * timeout, this reports the result normally. But if the test times out,
101     * this reports the timeout stack trace and begins the process of killing
102     * this no-longer-trustworthy process.
103     */
104    private void runWithTimeout(final Profiler profiler, final VogarTest test) {
105        testEnvironment.reset();
106        monitor.outcomeStarted(JUnitRunner.this, test.toString(), actionName);
107
108        // Start the test on a background thread.
109        final AtomicReference<Thread> executingThreadReference = new AtomicReference<Thread>();
110        Future<Throwable> result = executor.submit(new Callable<Throwable>() {
111            public Throwable call() throws Exception {
112                executingThreadReference.set(Thread.currentThread());
113                try {
114                    if (profiler != null) {
115                        profiler.start();
116                    }
117                    test.run();
118                    return null;
119                } catch (Throwable throwable) {
120                    return throwable;
121                } finally {
122                    if (profiler != null) {
123                        profiler.stop();
124                    }
125                }
126            }
127        });
128
129        // Wait until either the result arrives or the test times out.
130        Throwable thrown;
131        try {
132            thrown = timeoutSeconds == 0
133                    ? result.get()
134                    : result.get(timeoutSeconds, TimeUnit.SECONDS);
135        } catch (TimeoutException e) {
136            vmIsUnstable = true;
137            Thread executingThread = executingThreadReference.get();
138            if (executingThread != null) {
139                executingThread.interrupt();
140                e.setStackTrace(executingThread.getStackTrace());
141            }
142            thrown = e;
143        } catch (Exception e) {
144            thrown = e;
145        }
146
147        if (thrown != null) {
148            prepareForDisplay(thrown);
149            thrown.printStackTrace(System.out);
150            monitor.outcomeFinished(Result.EXEC_FAILED);
151        } else {
152            monitor.outcomeFinished(Result.SUCCESS);
153        }
154    }
155
156    /**
157     * Strip vogar's lines from the stack trace. For example, we'd strip the
158     * first two Assert lines and everything after the testFoo() line in this
159     * stack trace:
160     *
161   	 *   at junit.framework.Assert.fail(Assert.java:198)
162   	 *   at junit.framework.Assert.assertEquals(Assert.java:56)
163   	 *   at junit.framework.Assert.assertEquals(Assert.java:61)
164   	 *   at libcore.java.net.FooTest.testFoo(FooTest.java:124)
165   	 *   at java.lang.reflect.Method.invokeNative(Native Method)
166   	 *   at java.lang.reflect.Method.invoke(Method.java:491)
167   	 *   at vogar.target.junit.Junit$JUnitTest.run(Junit.java:214)
168   	 *   at vogar.target.junit.JUnitRunner$1.call(JUnitRunner.java:112)
169   	 *   at vogar.target.junit.JUnitRunner$1.call(JUnitRunner.java:105)
170   	 *   at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
171   	 *   at java.util.concurrent.FutureTask.run(FutureTask.java:137)
172   	 *   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
173   	 *   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:569)
174   	 *   at java.lang.Thread.run(Thread.java:863)
175     */
176    public void prepareForDisplay(Throwable t) {
177        StackTraceElement[] stackTraceElements = t.getStackTrace();
178        boolean foundVogar = false;
179
180        int last = stackTraceElements.length - 1;
181        for (; last >= 0; last--) {
182            String className = stackTraceElements[last].getClassName();
183            if (className.startsWith("vogar.target")) {
184                foundVogar = true;
185            } else if (foundVogar
186                    && !className.startsWith("java.lang.reflect")
187                    && !className.startsWith("sun.reflect")
188                    && !className.startsWith("junit.framework")) {
189                if (last < stackTraceElements.length) {
190                    last++;
191                }
192                break;
193            }
194        }
195
196        int first = 0;
197        for (; first < last; first++) {
198            String className = stackTraceElements[first].getClassName();
199            if (!className.startsWith("junit.framework")) {
200                break;
201            }
202        }
203
204        if (first > 0) {
205            first--; // retain one assertSomething() line in the trace
206        }
207
208        if (first < last) {
209            // Arrays.copyOfRange() didn't exist on Froyo
210            StackTraceElement[] copyOfRange = new StackTraceElement[last - first];
211            System.arraycopy(stackTraceElements, first, copyOfRange, 0, last - first);
212            t.setStackTrace(copyOfRange);
213        }
214    }
215
216    public boolean supports(Class<?> klass) {
217        return Junit3.isJunit3Test(klass) || Junit4.isJunit4Test(klass);
218    }
219}
220