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