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 
17 package vogar;
18 
19 import com.android.json.stream.JsonReader;
20 import com.google.common.base.Joiner;
21 import com.google.common.base.Splitter;
22 import com.google.common.collect.Iterables;
23 import java.io.File;
24 import java.io.FileReader;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.InputStreamReader;
28 import java.io.Reader;
29 import java.net.URL;
30 import java.util.Collections;
31 import java.util.LinkedHashMap;
32 import java.util.LinkedHashSet;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.regex.Pattern;
37 import vogar.commands.Command;
38 import vogar.util.Log;
39 
40 /**
41  * A database of expected outcomes. Entries in this database come in two forms.
42  * <ul>
43  *   <li>Outcome expectations name an outcome (or its prefix, such as
44  *       "java.util"), its expected result, and an optional pattern to match
45  *       the expected output.
46  *   <li>Failure expectations include a pattern that may match the output of any
47  *       outcome. These expectations are useful for hiding failures caused by
48  *       cross-cutting features that aren't supported.
49  * </ul>
50  *
51  * <p>If an outcome matches both an outcome expectation and a failure
52  * expectation, the outcome expectation will be returned.
53  */
54 public final class ExpectationStore {
55 
56     /** The pattern to use when no expected output is specified */
57     private static final Pattern MATCH_ALL_PATTERN
58             = Pattern.compile(".*", Pattern.MULTILINE | Pattern.DOTALL);
59 
60     /** The expectation of a general successful run. */
61     private static final Expectation SUCCESS = new Expectation(Result.SUCCESS, MATCH_ALL_PATTERN,
62             Collections.<String>emptySet(), "", -1);
63 
64     private static final int PATTERN_FLAGS = Pattern.MULTILINE | Pattern.DOTALL;
65 
66     private final Map<String, Expectation> outcomes = new LinkedHashMap<String, Expectation>();
67     private final Map<String, Expectation> failures = new LinkedHashMap<String, Expectation>();
68 
ExpectationStore()69     private ExpectationStore() {}
70 
71     /**
72      * Finds the expected result for the specified action or outcome name. This
73      * returns a value for all names, even if no explicit expectation was set.
74      */
get(String name)75     public Expectation get(String name) {
76         Expectation byName = getByNameOrPackage(name);
77         return byName != null ? byName : SUCCESS;
78     }
79 
80     /**
81      * Finds the expected result for the specified outcome after it has
82      * completed. Unlike {@code get()}, this also takes into account the
83      * outcome's output.
84      *
85      * <p>For outcomes that have both a name match and an output match,
86      * exact name matches are preferred, then output matches, then inexact
87      * name matches.
88      */
get(Outcome outcome)89     public Expectation get(Outcome outcome) {
90         Expectation exactNameMatch = outcomes.get(outcome.getName());
91         if (exactNameMatch != null) {
92             return exactNameMatch;
93         }
94 
95         for (Map.Entry<String, Expectation> entry : failures.entrySet()) {
96             if (entry.getValue().matches(outcome)) {
97                 return entry.getValue();
98             }
99         }
100 
101         Expectation byName = getByNameOrPackage(outcome.getName());
102         return byName != null ? byName : SUCCESS;
103     }
104 
getByNameOrPackage(String name)105     private Expectation getByNameOrPackage(String name) {
106         while (true) {
107             Expectation expectation = outcomes.get(name);
108             if (expectation != null) {
109                 return expectation;
110             }
111 
112             int dotOrHash = Math.max(name.lastIndexOf('.'), name.lastIndexOf('#'));
113             if (dotOrHash == -1) {
114                 return null;
115             }
116 
117             name = name.substring(0, dotOrHash);
118         }
119     }
120 
parse(Set<File> expectationFiles, ModeId mode)121     public static ExpectationStore parse(Set<File> expectationFiles, ModeId mode) throws IOException {
122         ExpectationStore result = new ExpectationStore();
123         for (File f : expectationFiles) {
124             if (f.exists()) {
125                 result.parse(f, mode);
126             }
127         }
128         return result;
129     }
130 
131     /**
132      * Create an {@link ExpectationStore} that is populated from expectation resources.
133      * @param owningClass the class from which the resources are loaded.
134      * @param expectationResources the set of paths to the expectation resources; the paths are
135      * either relative to the owning class, or absolute (starting with a /).
136      * @param mode the mode within which the tests are to be run.
137      * @return the populated {@link ExpectationStore}.
138      * @throws IOException if there was a problem loading
139      */
parseResources( Class<?> owningClass, Set<String> expectationResources, ModeId mode)140     public static ExpectationStore parseResources(
141             Class<?> owningClass, Set<String> expectationResources, ModeId mode)
142             throws IOException {
143         ExpectationStore result = new ExpectationStore();
144         for (String expectationsPath : expectationResources) {
145             URL url = owningClass.getResource(expectationsPath);
146             if (url == null) {
147                 Log.warn("Could not find resource '" + expectationsPath
148                         + "' relative to " + owningClass);
149             } else {
150                 result.parse(url, mode);
151             }
152         }
153         return result;
154     }
155 
parse(URL url, ModeId mode)156     private void parse(URL url, ModeId mode) throws IOException {
157         Log.verbose("loading expectations from " + url);
158 
159         try (InputStream is = url.openStream();
160              Reader reader = new InputStreamReader(is)) {
161             parse(reader, url.toString(), mode);
162         }
163     }
164 
parse(File expectationsFile, ModeId mode)165     public void parse(File expectationsFile, ModeId mode) throws IOException {
166         Log.verbose("loading expectations file " + expectationsFile);
167 
168         try (Reader fileReader = new FileReader(expectationsFile)) {
169             String source = expectationsFile.toString();
170             parse(fileReader, source, mode);
171         }
172     }
173 
parse(Reader reader, String source, ModeId mode)174     private void parse(Reader reader, String source, ModeId mode) throws IOException {
175         int count = 0;
176         try (JsonReader jsonReader = new JsonReader(reader)) {
177             jsonReader.setLenient(true);
178             jsonReader.beginArray();
179             while (jsonReader.hasNext()) {
180                 readExpectation(jsonReader, mode);
181                 count++;
182             }
183             jsonReader.endArray();
184 
185             Log.verbose("loaded " + count + " expectations from " + source);
186         }
187     }
188 
readExpectation(JsonReader reader, ModeId mode)189     private void readExpectation(JsonReader reader, ModeId mode) throws IOException {
190         boolean isFailure = false;
191         Result result = Result.EXEC_FAILED;
192         Pattern pattern = MATCH_ALL_PATTERN;
193         Set<String> names = new LinkedHashSet<String>();
194         Set<String> tags = new LinkedHashSet<String>();
195         Set<ModeId> modes = null;
196         String description = "";
197         long buganizerBug = -1;
198 
199         reader.beginObject();
200         while (reader.hasNext()) {
201             String name = reader.nextName();
202             if (name.equals("result")) {
203                 result = Result.valueOf(reader.nextString());
204             } else if (name.equals("name")) {
205                 names.add(reader.nextString());
206             } else if (name.equals("names")) {
207                 readStrings(reader, names);
208             } else if (name.equals("failure")) {
209                 // isFailure is somewhat arbitrarily keyed on the existence of a "failure"
210                 // element instead of looking at the "result" field. There are only about 5
211                 // expectations in our entire expectation store that have this tag.
212                 //
213                 // TODO: Get rid of it and the "failures" map and just use the outcomes
214                 // map for everything. Both uses seem useless.
215                 isFailure = true;
216                 names.add(reader.nextString());
217             } else if (name.equals("pattern")) {
218                 pattern = Pattern.compile(reader.nextString(), PATTERN_FLAGS);
219             } else if (name.equals("substring")) {
220                 pattern = Pattern.compile(".*" + Pattern.quote(reader.nextString()) + ".*", PATTERN_FLAGS);
221             } else if (name.equals("tags")) {
222                 readStrings(reader, tags);
223             } else if (name.equals("description")) {
224                 Iterable<String> split = Splitter.on("\n").omitEmptyStrings().trimResults().split(reader.nextString());
225                 description = Joiner.on("\n").join(split);
226             } else if (name.equals("bug")) {
227                 buganizerBug = reader.nextLong();
228             } else if (name.equals("modes")) {
229                 modes = readModes(reader);
230             } else {
231                 Log.warn("Unhandled name in expectations file: " + name);
232                 reader.skipValue();
233             }
234         }
235         reader.endObject();
236 
237         if (names.isEmpty()) {
238             throw new IllegalArgumentException("Missing 'name' or 'failure' key in " + reader);
239         }
240         if (modes != null && !modes.contains(mode)) {
241             return;
242         }
243 
244         Expectation expectation = new Expectation(result, pattern, tags, description, buganizerBug);
245         Map<String, Expectation> map = isFailure ? failures : outcomes;
246         for (String name : names) {
247             if (map.put(name, expectation) != null) {
248                 throw new IllegalArgumentException("Duplicate expectations for " + name);
249             }
250         }
251     }
252 
readStrings(JsonReader reader, Set<String> output)253     private void readStrings(JsonReader reader, Set<String> output) throws IOException {
254         reader.beginArray();
255         while (reader.hasNext()) {
256             output.add(reader.nextString());
257         }
258         reader.endArray();
259     }
260 
readModes(JsonReader reader)261     private Set<ModeId> readModes(JsonReader reader) throws IOException {
262         Set<ModeId> result = new LinkedHashSet<ModeId>();
263         reader.beginArray();
264         while (reader.hasNext()) {
265             result.add(ModeId.valueOf(reader.nextString().toUpperCase()));
266         }
267         reader.endArray();
268         return result;
269     }
270 
271     /**
272      * Sets the bugIsOpen status on all expectations by querying an external bug
273      * tracker.
274      */
loadBugStatuses(String openBugsCommand)275     public void loadBugStatuses(String openBugsCommand) {
276         Iterable<Expectation> allExpectations = Iterables.concat(outcomes.values(), failures.values());
277 
278         // figure out what bug IDs we're interested in
279         Set<String> bugs = new LinkedHashSet<String>();
280         for (Expectation expectation : allExpectations) {
281             if (expectation.getBug() != -1) {
282                 bugs.add(Long.toString(expectation.getBug()));
283             }
284         }
285         if (bugs.isEmpty()) {
286             return;
287         }
288 
289         // query the external app for open bugs
290         List<String> openBugs = new Command.Builder()
291                 .args(openBugsCommand)
292                 .args(bugs)
293                 .execute();
294         Set<Long> openBugsSet = new LinkedHashSet<Long>();
295         for (String bug : openBugs) {
296             openBugsSet.add(Long.parseLong(bug));
297         }
298 
299         Log.verbose("tracking " + openBugsSet.size() + " open bugs: " + openBugs);
300 
301         // update our expectations with that set
302         for (Expectation expectation : allExpectations) {
303             if (openBugsSet.contains(expectation.getBug())) {
304                 expectation.setBugIsOpen(true);
305             }
306         }
307     }
308 
getAllOutComes()309     public Map<String, Expectation> getAllOutComes() {
310         return outcomes;
311     }
312 
getAllFailures()313     public Map<String, Expectation> getAllFailures() {
314         return failures;
315     }
316 }
317