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 17package vogar; 18 19import com.google.common.base.Joiner; 20import com.google.common.base.Splitter; 21import com.google.common.collect.Iterables; 22import com.google.gson.stream.JsonReader; 23import java.io.File; 24import java.io.FileReader; 25import java.io.IOException; 26import java.util.EnumSet; 27import java.util.EnumMap; 28import java.util.LinkedHashMap; 29import java.util.LinkedHashSet; 30import java.util.Map; 31import java.util.Set; 32import java.util.regex.Pattern; 33 34/** 35 * A database of expected outcomes. Entries in this database come in two forms. 36 * <ul> 37 * <li>Outcome expectations name an outcome (or its prefix, such as 38 * "java.util"), its expected result, and an optional pattern to match 39 * the expected output. 40 * <li>Failure expectations include a pattern that may match the output of any 41 * outcome. These expectations are useful for hiding failures caused by 42 * cross-cutting features that aren't supported. 43 * </ul> 44 * 45 * <p>If an outcome matches both an outcome expectation and a failure 46 * expectation, the outcome expectation will be returned. 47 */ 48final class ExpectationStore { 49 private static final int PATTERN_FLAGS = Pattern.MULTILINE | Pattern.DOTALL; 50 51 private final Log log; 52 private final Map<String, Expectation> outcomes = new LinkedHashMap<String, Expectation>(); 53 private final Map<String, Expectation> failures = new LinkedHashMap<String, Expectation>(); 54 55 private ExpectationStore(Log log) { 56 this.log = log; 57 } 58 59 /** 60 * Finds the expected result for the specified action or outcome name. This 61 * returns a value for all names, even if no explicit expectation was set. 62 */ 63 public Expectation get(String name) { 64 Expectation byName = getByNameOrPackage(name); 65 return byName != null ? byName : Expectation.SUCCESS; 66 } 67 68 /** 69 * Finds the expected result for the specified outcome after it has 70 * completed. Unlike {@code get()}, this also takes into account the 71 * outcome's output. 72 * 73 * <p>For outcomes that have both a name match and an output match, 74 * exact name matches are preferred, then output matches, then inexact 75 * name matches. 76 */ 77 public Expectation get(Outcome outcome) { 78 Expectation exactNameMatch = outcomes.get(outcome.getName()); 79 if (exactNameMatch != null) { 80 return exactNameMatch; 81 } 82 83 for (Map.Entry<String, Expectation> entry : failures.entrySet()) { 84 if (entry.getValue().matches(outcome)) { 85 return entry.getValue(); 86 } 87 } 88 89 Expectation byName = getByNameOrPackage(outcome.getName()); 90 return byName != null ? byName : Expectation.SUCCESS; 91 } 92 93 private Expectation getByNameOrPackage(String name) { 94 while (true) { 95 Expectation expectation = outcomes.get(name); 96 if (expectation != null) { 97 return expectation; 98 } 99 100 int dotOrHash = Math.max(name.lastIndexOf('.'), name.lastIndexOf('#')); 101 if (dotOrHash == -1) { 102 return null; 103 } 104 105 name = name.substring(0, dotOrHash); 106 } 107 } 108 109 public static ExpectationStore parse(Log log, 110 Set<File> expectationFiles, 111 ModeId mode, 112 Variant variant) 113 throws IOException { 114 ExpectationStore result = new ExpectationStore(log); 115 for (File f : expectationFiles) { 116 if (f.exists()) { 117 result.parse(f, mode, variant); 118 } 119 } 120 return result; 121 } 122 123 public void parse(File expectationsFile, ModeId mode, Variant variant) throws IOException { 124 log.verbose("loading expectations file " + expectationsFile); 125 126 int count = 0; 127 JsonReader reader = null; 128 try { 129 reader = new JsonReader(new FileReader(expectationsFile)); 130 reader.setLenient(true); 131 reader.beginArray(); 132 while (reader.hasNext()) { 133 readExpectation(reader, mode, variant); 134 count++; 135 } 136 reader.endArray(); 137 138 log.verbose("loaded " + count + " expectations from " + expectationsFile); 139 } finally { 140 if (reader != null) { 141 reader.close(); 142 } 143 } 144 } 145 146 private void readExpectation(JsonReader reader, ModeId mode, Variant variant) 147 throws IOException { 148 boolean isFailure = false; 149 Result result = Result.SUCCESS; 150 Pattern pattern = Expectation.MATCH_ALL_PATTERN; 151 Set<String> names = new LinkedHashSet<String>(); 152 Set<String> tags = new LinkedHashSet<String>(); 153 Map<ModeId, Set<Variant>> modeVariants = null; 154 Set<ModeId> modes = null; 155 String description = ""; 156 long buganizerBug = -1; 157 158 reader.beginObject(); 159 while (reader.hasNext()) { 160 String name = reader.nextName(); 161 if (name.equals("result")) { 162 result = Result.valueOf(reader.nextString()); 163 } else if (name.equals("name")) { 164 names.add(reader.nextString()); 165 } else if (name.equals("names")) { 166 readStrings(reader, names); 167 } else if (name.equals("failure")) { 168 isFailure = true; 169 names.add(reader.nextString()); 170 } else if (name.equals("pattern")) { 171 pattern = Pattern.compile(reader.nextString(), PATTERN_FLAGS); 172 } else if (name.equals("substring")) { 173 pattern = Pattern.compile( 174 ".*" + Pattern.quote(reader.nextString()) + ".*", PATTERN_FLAGS); 175 } else if (name.equals("tags")) { 176 readStrings(reader, tags); 177 } else if (name.equals("description")) { 178 Iterable<String> split = Splitter.on("\n").omitEmptyStrings().trimResults() 179 .split(reader.nextString()); 180 description = Joiner.on("\n").join(split); 181 } else if (name.equals("bug")) { 182 buganizerBug = reader.nextLong(); 183 } else if (name.equals("modes")) { 184 modes = readModes(reader); 185 } else if (name.equals("modes_variants")) { 186 modeVariants = readModesAndVariants(reader); 187 } else { 188 log.warn("Unhandled name in expectations file: " + name); 189 reader.skipValue(); 190 } 191 } 192 reader.endObject(); 193 194 if (names.isEmpty()) { 195 throw new IllegalArgumentException("Missing 'name' or 'failure' key in " + reader); 196 } 197 if (modes != null && !modes.contains(mode)) { 198 return; 199 } 200 if (modeVariants != null) { 201 Set<Variant> variants = modeVariants.get(mode); 202 if (variants == null || !variants.contains(variant)) { 203 return; 204 } 205 } 206 207 Expectation expectation = 208 new Expectation(result, pattern, tags, description, buganizerBug, true); 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 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 225 private Set<ModeId> readModes(JsonReader reader) throws IOException { 226 Set<ModeId> result = EnumSet.noneOf(ModeId.class); 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 * Expected format: mode_variants: [["host", "X32"], ["host", "X64"]] 237 */ 238 private Map<ModeId, Set<Variant>> readModesAndVariants(JsonReader reader) throws IOException { 239 Map<ModeId, Set<Variant>> result = new EnumMap<ModeId, Set<Variant>>(ModeId.class); 240 reader.beginArray(); 241 while (reader.hasNext()) { 242 reader.beginArray(); 243 ModeId mode = ModeId.valueOf(reader.nextString().toUpperCase()); 244 Set<Variant> set = result.get(mode); 245 if (set == null) { 246 set = EnumSet.noneOf(Variant.class); 247 result.put(mode, set); 248 } 249 set.add(Variant.valueOf(reader.nextString().toUpperCase())); 250 // Note that the following checks that we are at the end of the array. 251 reader.endArray(); 252 } 253 reader.endArray(); 254 return result; 255 } 256 257 /** 258 * Sets the bugIsOpen status on all expectations by querying an external bug 259 * tracker. 260 */ 261 public void loadBugStatuses(BugDatabase bugDatabase) { 262 Iterable<Expectation> allExpectations 263 = Iterables.concat(outcomes.values(), failures.values()); 264 265 // figure out what bug IDs we're interested in 266 Set<Long> bugs = new LinkedHashSet<Long>(); 267 for (Expectation expectation : allExpectations) { 268 if (expectation.getBug() != -1) { 269 bugs.add(expectation.getBug()); 270 } 271 } 272 if (bugs.isEmpty()) { 273 return; 274 } 275 276 Set<Long> openBugs = bugDatabase.bugsToOpenBugs(bugs); 277 278 log.verbose("tracking " + openBugs.size() + " open bugs: " + openBugs); 279 280 // update our expectations with that set 281 for (Expectation expectation : allExpectations) { 282 if (openBugs.contains(expectation.getBug())) { 283 expectation.setBugIsOpen(true); 284 } 285 } 286 } 287 288 interface BugDatabase { 289 Set<Long> bugsToOpenBugs(Set<Long> bugs); 290 } 291 292} 293