1/*
2 * Copyright (C) 2011 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.tasks;
18
19import java.io.File;
20import java.io.IOException;
21import vogar.Action;
22import vogar.Classpath;
23import vogar.Outcome;
24import vogar.Result;
25import vogar.Run;
26import vogar.commands.Command;
27import vogar.commands.VmCommandBuilder;
28import vogar.monitor.HostMonitor;
29import vogar.target.CaliperRunner;
30import vogar.target.TestRunner;
31
32/**
33 * Executes a single action and then prints the result.
34 */
35public class RunActionTask extends Task implements HostMonitor.Handler {
36    /**
37     * Assign each runner thread a unique ID. Necessary so threads don't
38     * conflict when selecting a monitor port.
39     */
40    private final ThreadLocal<Integer> runnerThreadId = new ThreadLocal<Integer>() {
41        private int next = 0;
42        @Override protected synchronized Integer initialValue() {
43            return next++;
44        }
45    };
46
47    protected final Run run;
48    private final boolean useLargeTimeout;
49    private final Action action;
50    private final String actionName;
51    private Command currentCommand;
52    private String lastStartedOutcome;
53    private String lastFinishedOutcome;
54
55    public RunActionTask(Run run, Action action, boolean useLargeTimeout) {
56        super("run " + action.getName());
57        this.run = run;
58        this.action = action;
59        this.actionName = action.getName();
60        this.useLargeTimeout = useLargeTimeout;
61    }
62
63    @Override public boolean isAction() {
64        return true;
65    }
66
67    @Override protected Result execute() throws Exception {
68        run.console.action(actionName);
69
70        while (true) {
71            /*
72             * If the target process failed midway through a set of
73             * outcomes, that's okay. We pickup right after the first
74             * outcome that wasn't completed.
75             */
76            String skipPast = lastStartedOutcome;
77            lastStartedOutcome = null;
78
79            currentCommand = createActionCommand(action, skipPast, monitorPort(-1));
80            try {
81                currentCommand.start();
82
83                int timeoutSeconds = useLargeTimeout
84                        ? run.largeTimeoutSeconds
85                        : run.smallTimeoutSeconds;
86                if (timeoutSeconds != 0) {
87                    currentCommand.scheduleTimeout(timeoutSeconds);
88                }
89
90                HostMonitor hostMonitor = new HostMonitor(run.console, this);
91                boolean completedNormally = useSocketMonitor()
92                        ? hostMonitor.attach(monitorPort(run.firstMonitorPort))
93                        : hostMonitor.followStream(currentCommand.getInputStream());
94
95                if (completedNormally) {
96                    return Result.SUCCESS;
97                }
98
99                String earlyResultOutcome;
100                boolean giveUp;
101
102                if (lastStartedOutcome == null || lastStartedOutcome.equals(actionName)) {
103                    earlyResultOutcome = actionName;
104                    giveUp = true;
105                } else if (!lastStartedOutcome.equals(lastFinishedOutcome)) {
106                    earlyResultOutcome = lastStartedOutcome;
107                    giveUp = false;
108                } else {
109                    continue;
110                }
111
112                run.driver.addEarlyResult(new Outcome(earlyResultOutcome, Result.ERROR,
113                        "Action " + action + " did not complete normally.\n"
114                                + "timedOut=" + currentCommand.timedOut() + "\n"
115                                + "lastStartedOutcome=" + lastStartedOutcome + "\n"
116                                + "lastFinishedOutcome=" + lastFinishedOutcome + "\n"
117                                + "command=" + currentCommand));
118
119                if (giveUp) {
120                    return Result.ERROR;
121                }
122            } catch (IOException e) {
123                // if the monitor breaks, assume the worst and don't retry
124                run.driver.addEarlyResult(new Outcome(actionName, Result.ERROR, e));
125                return Result.ERROR;
126            } finally {
127                currentCommand.destroy();
128                currentCommand = null;
129            }
130        }
131    }
132
133    /**
134     * Create the command that executes the action.
135     *
136     * @param skipPast the last outcome to skip, or null to run all outcomes.
137     * @param monitorPort the port to accept connections on, or -1 for the
138     */
139    public Command createActionCommand(Action action, String skipPast, int monitorPort) {
140        File workingDirectory = action.getUserDir();
141        VmCommandBuilder vmCommandBuilder = run.mode.newVmCommandBuilder(action, workingDirectory);
142        Classpath runtimeClasspath = run.mode.getRuntimeClasspath(action);
143        if (run.useBootClasspath) {
144            vmCommandBuilder.bootClasspath(runtimeClasspath);
145        } else {
146            vmCommandBuilder.classpath(runtimeClasspath);
147        }
148        if (monitorPort != -1) {
149            vmCommandBuilder.args("--monitorPort", Integer.toString(monitorPort));
150        }
151        if (skipPast != null) {
152            vmCommandBuilder.args("--skipPast", skipPast);
153        }
154        return vmCommandBuilder
155                .temp(workingDirectory)
156                .debugPort(run.debugPort)
157                .vmArgs(run.additionalVmArgs)
158                .mainClass(TestRunner.class.getName())
159                .args(run.targetArgs)
160                .build();
161    }
162
163    /**
164     * Returns true if this mode requires a socket connection for reading test
165     * results. Otherwise all communication happens over the output stream of
166     * the forked process.
167     */
168    protected boolean useSocketMonitor() {
169        return false;
170    }
171
172    private int monitorPort(int defaultValue) {
173        return run.maxConcurrentActions == 1
174                ? defaultValue
175                : run.firstMonitorPort + runnerThreadId.get();
176    }
177
178    @Override public void start(String outcomeName, String runnerClass) {
179        outcomeName = toQualifiedOutcomeName(outcomeName);
180        lastStartedOutcome = outcomeName;
181        // TODO add to Outcome knowledge about what class was used to run it
182        if (CaliperRunner.class.getName().equals(runnerClass)) {
183            if (!run.benchmark) {
184                throw new RuntimeException("you must use --benchmark when running Caliper "
185                        + "benchmarks.");
186            }
187            run.console.verbose("running " + outcomeName + " with unlimited timeout");
188            Command command = currentCommand;
189            if (command != null && run.smallTimeoutSeconds != 0) {
190                command.scheduleTimeout(run.smallTimeoutSeconds);
191            }
192            run.driver.recordResults = false;
193        } else {
194            run.driver.recordResults = true;
195        }
196    }
197
198    @Override public void output(String outcomeName, String output) {
199        outcomeName = toQualifiedOutcomeName(outcomeName);
200        run.console.outcome(outcomeName);
201        run.console.streamOutput(outcomeName, output);
202    }
203
204    @Override public void finish(Outcome outcome) {
205        Command command = currentCommand;
206        if (command != null && run.smallTimeoutSeconds != 0) {
207            command.scheduleTimeout(run.smallTimeoutSeconds);
208        }
209        lastFinishedOutcome = toQualifiedOutcomeName(outcome.getName());
210        // TODO: support flexible timeouts for JUnit tests
211        run.driver.recordOutcome(new Outcome(lastFinishedOutcome, outcome.getResult(),
212                outcome.getOutputLines()));
213    }
214
215    /**
216     * Test suites that use main classes in the default package have lame
217     * outcome names like "Clear" rather than "com.foo.Bar.Clear". In that
218     * case, just replace the outcome name with the action name.
219     */
220    private String toQualifiedOutcomeName(String outcomeName) {
221        if (actionName.endsWith("." + outcomeName)
222                && !outcomeName.contains(".") && !outcomeName.contains("#")) {
223            return actionName;
224        } else {
225            return outcomeName;
226        }
227    }
228
229    @Override public void print(String string) {
230        run.console.streamOutput(string);
231    }
232}
233