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