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