1/*
2 * Copyright (C) 2009 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;
18
19import java.io.File;
20import java.io.IOException;
21import java.io.InputStream;
22import java.io.PrintStream;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.HashSet;
26import java.util.Iterator;
27import java.util.List;
28import java.util.Properties;
29import java.util.Set;
30import java.util.concurrent.atomic.AtomicReference;
31import vogar.Result;
32import vogar.TestProperties;
33import vogar.monitor.TargetMonitor;
34import vogar.target.junit.JUnitRunner;
35
36/**
37 * Runs an action, in process on the target.
38 */
39public final class TestRunner {
40
41    protected final Properties properties;
42
43    protected final String qualifiedName;
44    protected final String qualifiedClassOrPackageName;
45
46    /** the monitor port if a monitor is expected, or null for no monitor */
47    protected final Integer monitorPort;
48
49    /** use an atomic reference so the runner can null it out when it is encountered. */
50    protected final AtomicReference<String> skipPastReference;
51    protected final int timeoutSeconds;
52    protected final List<Runner> runners;
53    private final boolean profile;
54    private final int profileDepth;
55    private final int profileInterval;
56    private final File profileFile;
57    private final boolean profileThreadGroup;
58    protected final String[] args;
59    private boolean useSocketMonitor;
60
61    public TestRunner(List<String> argsList) {
62        properties = loadProperties();
63        qualifiedName = properties.getProperty(TestProperties.QUALIFIED_NAME);
64        qualifiedClassOrPackageName = properties.getProperty(TestProperties.TEST_CLASS_OR_PACKAGE);
65        timeoutSeconds = Integer.parseInt(properties.getProperty(TestProperties.TIMEOUT));
66
67        int monitorPort = Integer.parseInt(properties.getProperty(TestProperties.MONITOR_PORT));
68        String skipPast = null;
69        boolean profile = Boolean.parseBoolean(properties.getProperty(TestProperties.PROFILE));
70        int profileDepth = Integer.parseInt(properties.getProperty(TestProperties.PROFILE_DEPTH));
71        int profileInterval
72                = Integer.parseInt(properties.getProperty(TestProperties.PROFILE_INTERVAL));
73        File profileFile = new File(properties.getProperty(TestProperties.PROFILE_FILE));
74        boolean profileThreadGroup
75                = Boolean.parseBoolean(properties.getProperty(TestProperties.PROFILE_THREAD_GROUP));
76
77        boolean testOnly = Boolean.parseBoolean(properties.getProperty(TestProperties.TEST_ONLY));
78        if (testOnly) {
79          runners = Arrays.asList((Runner)new JUnitRunner());
80        } else {
81          runners = Arrays.asList(new JUnitRunner(),
82                                  new CaliperRunner(),
83                                  new MainRunner());
84        }
85        for (Iterator<String> i = argsList.iterator(); i.hasNext(); ) {
86            String arg = i.next();
87            if (arg.equals("--monitorPort")) {
88                i.remove();
89                monitorPort = Integer.parseInt(i.next());
90                i.remove();
91            }
92            if (arg.equals("--skipPast")) {
93                i.remove();
94                skipPast = i.next();
95                i.remove();
96            }
97        }
98
99        this.monitorPort = monitorPort;
100        this.skipPastReference = new AtomicReference<String>(skipPast);
101        this.profile = profile;
102        this.profileDepth = profileDepth;
103        this.profileInterval = profileInterval;
104        this.profileFile = profileFile;
105        this.profileThreadGroup = profileThreadGroup;
106        this.args = argsList.toArray(new String[argsList.size()]);
107    }
108
109    private Properties loadProperties() {
110        try {
111            InputStream in = getPropertiesStream();
112            Properties properties = new Properties();
113            properties.load(in);
114            in.close();
115            return properties;
116        } catch (IOException e) {
117            throw new RuntimeException(e);
118        }
119    }
120
121    /**
122     * Configure this test runner to await an incoming socket connection when
123     * writing test results. Otherwise all communication happens over
124     * System.out.
125     */
126    public void useSocketMonitor() {
127        this.useSocketMonitor = true;
128    }
129
130    /**
131     * Attempt to load the test properties file from both the application and system classloader.
132     * This is necessary because sometimes we run tests from the boot classpath.
133     */
134    private InputStream getPropertiesStream() throws IOException {
135        for (Class<?> classToLoadFrom : new Class<?>[] { TestRunner.class, Object.class }) {
136            InputStream propertiesStream = classToLoadFrom.getResourceAsStream(
137                    "/" + TestProperties.FILE);
138            if (propertiesStream != null) {
139                return propertiesStream;
140            }
141        }
142        throw new IOException(TestProperties.FILE + " missing!");
143    }
144
145    /**
146     * Returns the class to run the test with based on {@param klass}. For instance, a class
147     * that extends junit.framework.TestCase should be run with JUnitSpec.
148     *
149     * Returns null if no such associated runner exists.
150     */
151    private Class<?> runnerClass(Class<?> klass) {
152        for (Runner runner : runners) {
153            if (runner.supports(klass)) {
154                return runner.getClass();
155            }
156        }
157
158        return null;
159    }
160
161    public void run() throws IOException {
162        final TargetMonitor monitor = useSocketMonitor
163                ? TargetMonitor.await(monitorPort)
164                : TargetMonitor.forPrintStream(System.out);
165
166        PrintStream monitorPrintStream = new PrintStreamDecorator(System.out) {
167            @Override public void print(String str) {
168                monitor.output(str != null ? str : "null");
169            }
170        };
171        System.setOut(monitorPrintStream);
172        System.setErr(monitorPrintStream);
173
174        try {
175            run(monitor);
176        } catch (Throwable internalError) {
177            internalError.printStackTrace(monitorPrintStream);
178        } finally {
179            monitor.close();
180        }
181    }
182
183    public void run(final TargetMonitor monitor) {
184        TestEnvironment testEnvironment = new TestEnvironment();
185        testEnvironment.reset();
186
187        String classOrPackageName;
188        String qualification;
189
190        // Check whether the class or package is qualified and, if so, strip it off and pass it
191        // separately to the runners. For instance, may qualify a junit class by appending
192        // #method_name, where method_name is the name of a single test of the class to run.
193        int hash_position = qualifiedClassOrPackageName.indexOf("#");
194        if (hash_position != -1) {
195            classOrPackageName = qualifiedClassOrPackageName.substring(0, hash_position);
196            qualification = qualifiedClassOrPackageName.substring(hash_position + 1);
197        } else {
198            classOrPackageName = qualifiedClassOrPackageName;
199            qualification = null;
200        }
201
202        Set<Class<?>> classes = new ClassFinder().find(classOrPackageName);
203
204        // if there is more than one class in the set, this must be a package. Since we're
205        // running everything in the package already, remove any class called AllTests.
206        if (classes.size() > 1) {
207            Set<Class<?>> toRemove = new HashSet<Class<?>>();
208            for (Class<?> klass : classes) {
209                if (klass.getName().endsWith(".AllTests")) {
210                    toRemove.add(klass);
211                }
212            }
213            classes.removeAll(toRemove);
214        }
215
216
217        Profiler profiler = null;
218        if (profile) {
219            try {
220                profiler = Profiler.getInstance();
221            } catch (Exception e) {
222                System.out.println("Profiling is disabled: " + e);
223            }
224        }
225        if (profiler != null) {
226            profiler.setup(profileThreadGroup, profileDepth, profileInterval);
227        }
228        for (Class<?> klass : classes) {
229            Class<?> runnerClass = runnerClass(klass);
230            if (runnerClass == null) {
231                monitor.outcomeStarted(null, klass.getName(), qualifiedName);
232                System.out.println("Skipping " + klass.getName()
233                        + ": no associated runner class");
234                monitor.outcomeFinished(Result.UNSUPPORTED);
235                continue;
236            }
237
238            Runner runner;
239            try {
240                runner = (Runner) runnerClass.newInstance();
241                runner.init(monitor, qualifiedName, qualification, klass, skipPastReference,
242                        testEnvironment, timeoutSeconds, profile);
243            } catch (Exception e) {
244                monitor.outcomeStarted(null, qualifiedName, qualifiedName);
245                e.printStackTrace();
246                monitor.outcomeFinished(Result.ERROR);
247                return;
248            }
249            boolean completedNormally = runner.run(qualifiedName, profiler, args);
250            if (!completedNormally) {
251                return; // let the caller start another process
252            }
253        }
254        if (profiler != null) {
255            profiler.shutdown(profileFile);
256        }
257
258        monitor.completedNormally(true);
259    }
260
261    public static void main(String[] args) throws IOException {
262        new TestRunner(new ArrayList<String>(Arrays.asList(args))).run();
263        System.exit(0);
264    }
265
266}
267