/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package vogar; //import com.google.caliper.internal.gson.stream.JsonReader; import com.android.json.stream.JsonReader; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import vogar.commands.Command; import vogar.util.Log; /** * A database of expected outcomes. Entries in this database come in two forms. * <ul> * <li>Outcome expectations name an outcome (or its prefix, such as * "java.util"), its expected result, and an optional pattern to match * the expected output. * <li>Failure expectations include a pattern that may match the output of any * outcome. These expectations are useful for hiding failures caused by * cross-cutting features that aren't supported. * </ul> * * <p>If an outcome matches both an outcome expectation and a failure * expectation, the outcome expectation will be returned. */ public final class ExpectationStore { private static final int PATTERN_FLAGS = Pattern.MULTILINE | Pattern.DOTALL; private final Map<String, Expectation> outcomes = new LinkedHashMap<String, Expectation>(); private final Map<String, Expectation> failures = new LinkedHashMap<String, Expectation>(); private ExpectationStore() {} /** * Finds the expected result for the specified action or outcome name. This * returns a value for all names, even if no explicit expectation was set. */ public Expectation get(String name) { Expectation byName = getByNameOrPackage(name); return byName != null ? byName : Expectation.SUCCESS; } /** * Finds the expected result for the specified outcome after it has * completed. Unlike {@code get()}, this also takes into account the * outcome's output. * * <p>For outcomes that have both a name match and an output match, * exact name matches are preferred, then output matches, then inexact * name matches. */ public Expectation get(Outcome outcome) { Expectation exactNameMatch = outcomes.get(outcome.getName()); if (exactNameMatch != null) { return exactNameMatch; } for (Map.Entry<String, Expectation> entry : failures.entrySet()) { if (entry.getValue().matches(outcome)) { return entry.getValue(); } } Expectation byName = getByNameOrPackage(outcome.getName()); return byName != null ? byName : Expectation.SUCCESS; } private Expectation getByNameOrPackage(String name) { while (true) { Expectation expectation = outcomes.get(name); if (expectation != null) { return expectation; } int dotOrHash = Math.max(name.lastIndexOf('.'), name.lastIndexOf('#')); if (dotOrHash == -1) { return null; } name = name.substring(0, dotOrHash); } } public static ExpectationStore parse(Set<File> expectationFiles, ModeId mode) throws IOException { ExpectationStore result = new ExpectationStore(); for (File f : expectationFiles) { if (f.exists()) { result.parse(f, mode); } } return result; } public void parse(File expectationsFile, ModeId mode) throws IOException { Log.verbose("loading expectations file " + expectationsFile); int count = 0; JsonReader reader = null; try { reader = new JsonReader(new FileReader(expectationsFile)); reader.setLenient(true); reader.beginArray(); while (reader.hasNext()) { readExpectation(reader, mode); count++; } reader.endArray(); Log.verbose("loaded " + count + " expectations from " + expectationsFile); } finally { if (reader != null) { reader.close(); } } } private void readExpectation(JsonReader reader, ModeId mode) throws IOException { boolean isFailure = false; Result result = Result.SUCCESS; Pattern pattern = Expectation.MATCH_ALL_PATTERN; Set<String> names = new LinkedHashSet<String>(); Set<String> tags = new LinkedHashSet<String>(); Set<ModeId> modes = null; String description = ""; long buganizerBug = -1; reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("result")) { result = Result.valueOf(reader.nextString()); } else if (name.equals("name")) { names.add(reader.nextString()); } else if (name.equals("names")) { readStrings(reader, names); } else if (name.equals("failure")) { isFailure = true; names.add(reader.nextString()); } else if (name.equals("pattern")) { pattern = Pattern.compile(reader.nextString(), PATTERN_FLAGS); } else if (name.equals("substring")) { pattern = Pattern.compile(".*" + Pattern.quote(reader.nextString()) + ".*", PATTERN_FLAGS); } else if (name.equals("tags")) { readStrings(reader, tags); } else if (name.equals("description")) { Iterable<String> split = Splitter.on("\n").omitEmptyStrings().trimResults().split(reader.nextString()); description = Joiner.on("\n").join(split); } else if (name.equals("bug")) { buganizerBug = reader.nextLong(); } else if (name.equals("modes")) { modes = readModes(reader); } else { Log.warn("Unhandled name in expectations file: " + name); reader.skipValue(); } } reader.endObject(); if (names.isEmpty()) { throw new IllegalArgumentException("Missing 'name' or 'failure' key in " + reader); } if (modes != null && !modes.contains(mode)) { return; } Expectation expectation = new Expectation(result, pattern, tags, description, buganizerBug); Map<String, Expectation> map = isFailure ? failures : outcomes; for (String name : names) { if (map.put(name, expectation) != null) { throw new IllegalArgumentException("Duplicate expectations for " + name); } } } private void readStrings(JsonReader reader, Set<String> output) throws IOException { reader.beginArray(); while (reader.hasNext()) { output.add(reader.nextString()); } reader.endArray(); } private Set<ModeId> readModes(JsonReader reader) throws IOException { Set<ModeId> result = new LinkedHashSet<ModeId>(); reader.beginArray(); while (reader.hasNext()) { result.add(ModeId.valueOf(reader.nextString().toUpperCase())); } reader.endArray(); return result; } /** * Sets the bugIsOpen status on all expectations by querying an external bug * tracker. */ public void loadBugStatuses(String openBugsCommand) { Iterable<Expectation> allExpectations = Iterables.concat(outcomes.values(), failures.values()); // figure out what bug IDs we're interested in Set<String> bugs = new LinkedHashSet<String>(); for (Expectation expectation : allExpectations) { if (expectation.getBug() != -1) { bugs.add(Long.toString(expectation.getBug())); } } if (bugs.isEmpty()) { return; } // query the external app for open bugs List<String> openBugs = new Command.Builder() .args(openBugsCommand) .args(bugs) .execute(); Set<Long> openBugsSet = new LinkedHashSet<Long>(); for (String bug : openBugs) { openBugsSet.add(Long.parseLong(bug)); } Log.verbose("tracking " + openBugsSet.size() + " open bugs: " + openBugs); // update our expectations with that set for (Expectation expectation : allExpectations) { if (openBugsSet.contains(expectation.getBug())) { expectation.setBugIsOpen(true); } } } }