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