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