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