1 /*
2  * Copyright (C) 2020 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 package com.android.timezone.location.validation;
17 
18 import static java.util.function.Function.identity;
19 import static java.util.stream.Collectors.toList;
20 import static java.util.stream.Collectors.toMap;
21 
22 import com.android.timezone.location.validation.proto.ValidationProtos;
23 
24 import com.google.common.geometry.S2CellId;
25 import com.google.protobuf.Message;
26 import com.google.protobuf.TextFormat;
27 
28 import java.io.File;
29 import java.io.FileReader;
30 import java.io.IOException;
31 import java.io.StringWriter;
32 import java.net.URI;
33 import java.util.ArrayList;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Objects;
38 import java.util.Set;
39 
40 /** Support classes and methods associated with geolocation data comparison / validation. */
41 class Types {
42 
Types()43     private Types() {
44     }
45 
46     /** A collection of known differences from a KnownDifferences proto file. */
47     static class KnownDifferences {
48 
49         private final List<KnownDifference> mKnownDifferences;
50 
KnownDifferences(List<KnownDifference> knownDifferences)51         private KnownDifferences(List<KnownDifference> knownDifferences) {
52             mKnownDifferences = Objects.requireNonNull(knownDifferences);
53         }
54 
55         /** Creates a {@link KnownDifferences} from a list of {@link KnownDifference} objects. */
create(List<KnownDifference> knownDifferences)56         static KnownDifferences create(List<KnownDifference> knownDifferences) {
57             return new KnownDifferences(new ArrayList<>(knownDifferences));
58         }
59 
60         /** Loads a known differences proto txt format file. */
load(File inputFile)61         static KnownDifferences load(File inputFile) throws IOException {
62             ValidationProtos.KnownDifferences.Builder builder =
63                     ValidationProtos.KnownDifferences.newBuilder();
64             try (FileReader reader = new FileReader(inputFile)) {
65                 TextFormat.getParser().merge(reader, builder);
66             }
67             ValidationProtos.KnownDifferences knownDifferencesProto = builder.build();
68 
69             List<KnownDifference> knownDifferenceList =
70                     knownDifferencesProto.getKnownDifferencesList()
71                             .stream()
72                             .map(KnownDifference::fromProto)
73                             .collect(toList());
74             return KnownDifferences.create(knownDifferenceList);
75         }
76 
77         /** Builds a map from the known differences. Each difference is mapped from a {@link
78          * TestCaseId} to the {@link KnownDifference} it came from. */
buildIdMap()79         Map<TestCaseId, KnownDifference> buildIdMap() {
80             return mKnownDifferences.stream()
81                     .collect(toMap(KnownDifference::getTestCaseId, identity()));
82         }
83 
toProto()84         private ValidationProtos.KnownDifferences toProto() {
85             List<ValidationProtos.KnownDifference> knownDifferenceProtos = new ArrayList<>();
86             for (KnownDifference knownDifference : mKnownDifferences) {
87                 knownDifferenceProtos.add(knownDifference.toProto());
88             }
89             return ValidationProtos.KnownDifferences.newBuilder()
90                     .addAllKnownDifferences(knownDifferenceProtos)
91                     .build();
92         }
93 
94         /** Returns the proto txt string form for the known differences. */
toProtoText()95         String toProtoText() {
96             ValidationProtos.KnownDifferences knownDifferencesProto = toProto();
97             return Types.toProtoText(knownDifferencesProto);
98         }
99     }
100 
101     /**
102      * An identifier for an individual city test case. It consists of a city's name and its
103      * location.
104      */
105     static class TestCaseId {
106 
107         private final String mCityName;
108 
109         private final S2CellId mCellId;
110 
111         /** Creates a city test case identifier. */
TestCaseId(String cityName, S2CellId cellId)112         TestCaseId(String cityName, S2CellId cellId) {
113             this.mCityName = Objects.requireNonNull(cityName);
114             this.mCellId = Objects.requireNonNull(cellId);
115         }
116 
getCityName()117         String getCityName() {
118             return mCityName;
119         }
120 
getCellId()121         S2CellId getCellId() {
122             return mCellId;
123         }
124 
125         @Override
equals(Object o)126         public boolean equals(Object o) {
127             if (this == o) {
128                 return true;
129             }
130             if (o == null || getClass() != o.getClass()) {
131                 return false;
132             }
133             TestCaseId that = (TestCaseId) o;
134             return mCityName.equals(that.mCityName)
135                     && mCellId.equals(that.mCellId);
136         }
137 
138         @Override
hashCode()139         public int hashCode() {
140             return Objects.hash(mCityName, mCellId);
141         }
142 
143         @Override
toString()144         public String toString() {
145             return "TestCaseId{"
146                     + "mCityName='" + mCityName + '\''
147                     + ", mCellId=" + mCellId
148                     + '}';
149         }
150     }
151 
152     /**
153      * Represents a case where there is a recorded {@link KnownDifference}, but where the
154      * actual data didn't match it somehow.
155      */
156     static class KnownDifferenceMismatch {
157         /** The referenceData known difference. */
158         private final KnownDifference mKnownDifference;
159 
160         /** The expected result according to the reference data. */
161         private final Result mReferenceDataResult;
162 
163         /** The actual result. */
164         private final Result mActualResult;
165 
KnownDifferenceMismatch(KnownDifference knownDifference, Result referenceDataResult, Result actualResult)166         KnownDifferenceMismatch(KnownDifference knownDifference,
167                 Result referenceDataResult, Result actualResult) {
168             this.mKnownDifference = Objects.requireNonNull(knownDifference);
169             this.mReferenceDataResult = Objects.requireNonNull(referenceDataResult);
170             this.mActualResult = Objects.requireNonNull(actualResult);
171         }
172 
getReferenceDataKnownDifference()173         KnownDifference getReferenceDataKnownDifference() {
174             return mKnownDifference;
175         }
176 
getActualKnownDifference()177         KnownDifference getActualKnownDifference() {
178             return new KnownDifference(mKnownDifference.mTestCaseId, mReferenceDataResult,
179                     mActualResult,
180                     mKnownDifference.mType, mKnownDifference.mComment, mKnownDifference.mBugUri);
181         }
182     }
183 
184     /** An historic known difference between an actual result and reference data. */
185     static class KnownDifference {
186 
fromProto( ValidationProtos.KnownDifference knownDifferenceProto)187         static KnownDifference fromProto(
188                 ValidationProtos.KnownDifference knownDifferenceProto) {
189             TestCaseId testCaseId = new TestCaseId(
190                     knownDifferenceProto.getCityName(),
191                     new S2CellId(knownDifferenceProto.getS2CellId()));
192             return new KnownDifference(
193                     testCaseId,
194                     Result.fromProto(knownDifferenceProto.getReferenceDataResult()),
195                     Result.fromProto(knownDifferenceProto.getActualResult()),
196                     Type.valueOf(knownDifferenceProto.getType()),
197                     knownDifferenceProto.getComment(),
198                     URI.create(knownDifferenceProto.getBugUri())
199             );
200         }
201 
toProto()202         ValidationProtos.KnownDifference toProto() {
203             return ValidationProtos.KnownDifference.newBuilder()
204                     .setCityName(mTestCaseId.getCityName())
205                     .setS2CellId(mTestCaseId.getCellId().id())
206                     .setReferenceDataResult(mReferenceDataResult.toProto())
207                     .setActualResult(mActualResult.toProto())
208                     .setComment(mComment)
209                     .setType(mType.toString())
210                     .setBugUri(mBugUri.toString())
211                     .build();
212         }
213 
214         /** A categorization for causes of known differences. */
215         enum Type {
216             /** Uncategorized. */
217             UNKNOWN,
218             /** A known bug in the data generation pipeline. */
219             PIPELINE_BUG,
220             /** A difference due to geopolitical position. */
221             GEOPOLITICS,
222             /** A generic difference in data not generated by Android. */
223             UPSTREAM_DIFFERENCE,
224         }
225 
226         private final TestCaseId mTestCaseId;
227 
228         private final Result mReferenceDataResult;
229 
230         private final Result mActualResult;
231 
232         /** A broad categorization of the cause for the difference. */
233         private final Type mType;
234 
235         /** Free-form details about the difference. */
236         private final String mComment;
237 
238         /** A bug URL tracking the investigation / fix for the difference, if there is one. */
239         private final URI mBugUri;
240 
KnownDifference(TestCaseId testCaseId, Result referenceDataResult, Result actualResult, Type type, String comment, URI bugUri)241         KnownDifference(TestCaseId testCaseId, Result referenceDataResult,
242                 Result actualResult, Type type, String comment, URI bugUri) {
243             this.mTestCaseId = Objects.requireNonNull(testCaseId);
244             this.mReferenceDataResult = Objects.requireNonNull(referenceDataResult);
245             this.mActualResult = Objects.requireNonNull(actualResult);
246             this.mType = Objects.requireNonNull(type);
247             this.mComment = Objects.requireNonNull(comment);
248             this.mBugUri = Objects.requireNonNull(bugUri);
249         }
250 
251         /** Returns the identifier for the test case that differed. */
getTestCaseId()252         TestCaseId getTestCaseId() {
253             return mTestCaseId;
254         }
255 
256         /** Returns the answer according to the reference data set. */
getReferenceDataResult()257         Result getReferenceDataResult() {
258             return mReferenceDataResult;
259         }
260 
261         /** REturns the answer according to Android's data set. */
getActualResult()262         Result getActualResult() {
263             return mActualResult;
264         }
265 
toProtoText()266         String toProtoText() {
267             ValidationProtos.KnownDifference proto = toProto();
268             return Types.toProtoText(proto);
269         }
270     }
271 
toProtoText(Message proto)272     private static String toProtoText(Message proto) {
273         try (StringWriter writer = new StringWriter()) {
274             TextFormat.print(proto, writer);
275             return writer.getBuffer().toString();
276         } catch (IOException e) {
277             throw new IllegalStateException("This will never happen", e);
278         }
279     }
280 
281     /** The result of a city lookup. */
282     static class Result {
283 
284         private final List<String> mIsoCountryCodes;
285         private final List<String> mZoneIds;
286 
Result(List<String> isoCountryCodes, List<String> zoneIds)287         Result(List<String> isoCountryCodes, List<String> zoneIds) {
288             this.mIsoCountryCodes = isoCountryCodes;
289             this.mZoneIds = new ArrayList<>(zoneIds);
290         }
291 
hasMultipleZoneIds()292         boolean hasMultipleZoneIds() {
293             return mZoneIds.size() > 1;
294         }
295 
296         /**
297          * Returns {@code true} if there is an intersection between the country codes and zone IDs.
298          */
intersects(Result other)299         boolean intersects(Result other) {
300             boolean zonesIntersect = intersect(mZoneIds, other.mZoneIds);
301             boolean countriesIntersect = intersect(mIsoCountryCodes, other.mIsoCountryCodes);
302             return countriesIntersect && zonesIntersect;
303         }
304 
intersect(List<T> one, List<T> two)305         private static <T> boolean intersect(List<T> one, List<T> two) {
306             Set<T> intersectionSet = new HashSet<>(one);
307             intersectionSet.retainAll(two);
308             return !intersectionSet.isEmpty();
309         }
310 
toProto()311         ValidationProtos.Result toProto() {
312             return ValidationProtos.Result.newBuilder()
313                     .addAllIsoCountryCodes(mIsoCountryCodes)
314                     .addAllZoneIds(mZoneIds)
315                     .build();
316         }
317 
fromProto(ValidationProtos.Result proto)318         static Result fromProto(ValidationProtos.Result proto) {
319             return new Result(proto.getIsoCountryCodesList(), proto.getZoneIdsList());
320         }
321 
322         @Override
equals(Object o)323         public boolean equals(Object o) {
324             if (this == o) {
325                 return true;
326             }
327             if (o == null || getClass() != o.getClass()) {
328                 return false;
329             }
330             Result result = (Result) o;
331             return mIsoCountryCodes.equals(result.mIsoCountryCodes)
332                     && mZoneIds.equals(result.mZoneIds);
333         }
334 
335         @Override
hashCode()336         public int hashCode() {
337             return Objects.hash(mIsoCountryCodes, mZoneIds);
338         }
339 
340         @Override
toString()341         public String toString() {
342             return "Result{"
343                     + "mIsoCountryCodes=" + mIsoCountryCodes
344                     + ", mZoneIds=" + mZoneIds
345                     + '}';
346         }
347     }
348 }
349