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