Home | History | Annotate | Download | only in vogar
      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 
     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      */
     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      */
     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 
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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      */
    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 
    309     public Map<String, Expectation> getAllOutComes() {
    310         return outcomes;
    311     }
    312 
    313     public Map<String, Expectation> getAllFailures() {
    314         return failures;
    315     }
    316 }
    317