1 /*
2  * Copyright (C) 2022 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 com.android.adservices.service.topics.classifier;
18 
19 import static com.android.adservices.service.topics.classifier.CommonClassifierHelper.computeClassifierAssetChecksum;
20 import static com.android.adservices.service.topics.classifier.CommonClassifierHelper.getTopTopics;
21 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import static org.junit.Assert.assertThrows;
26 
27 import com.android.adservices.MockRandom;
28 import com.android.adservices.common.AdServicesExtendedMockitoTestCase;
29 import com.android.adservices.data.topics.Topic;
30 import com.android.adservices.service.FlagsFactory;
31 import com.android.adservices.service.stats.AdServicesLogger;
32 import com.android.adservices.service.stats.EpochComputationGetTopTopicsStats;
33 import com.android.adservices.shared.testing.SkipLoggingUsageRule;
34 import com.android.modules.utils.testing.ExtendedMockitoRule.SpyStatic;
35 
36 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
37 import com.google.common.collect.ImmutableList;
38 import com.google.common.collect.ImmutableMap;
39 import com.google.mobiledatadownload.ClientConfigProto.ClientFile;
40 
41 import org.junit.Before;
42 import org.junit.Test;
43 import org.mockito.ArgumentCaptor;
44 import org.mockito.Mock;
45 
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Random;
52 import java.util.stream.Collectors;
53 
54 /**
55  * Tests for {@link CommonClassifierHelper}.
56  *
57  * <p><b> Note: Some tests in this test class are depend on the ordering of topicIds in
58  * adservices/tests/unittest/service-core/assets/classifier/labels_test_topics.txt, because we will
59  * use Random() or MockRandom() to generate random integer index to get random topicIds. Topics will
60  * be selected from the topics list in order by their index in the topics list. </b>
61  */
62 @SpyStatic(FlagsFactory.class)
63 // TODO (b/359964245): Remove after bug is resolved.
64 @SkipLoggingUsageRule(reason = "b/359964245")
65 public final class CommonClassifierHelperTest extends AdServicesExtendedMockitoTestCase {
66     private static final String TEST_LABELS_FILE_PATH = "classifier/labels_test_topics.txt";
67     private static final String TEST_PRECOMPUTED_FILE_PATH =
68             "classifier/precomputed_test_app_list.csv";
69     private static final String TEST_CLASSIFIER_ASSETS_METADATA_PATH =
70             "classifier/classifier_test_assets_metadata.json";
71     private static final String TEST_CLASSIFIER_INPUT_CONFIG_PATH =
72             "classifier/classifier_input_config.txt";
73     private static final String PRODUCTION_LABELS_FILE_PATH = "classifier/labels_topics.txt";
74     private static final String PRODUCTION_APPS_FILE_PATH = "classifier/precomputed_app_list.csv";
75     private static final String PRODUCTION_CLASSIFIER_ASSETS_METADATA_PATH =
76             "classifier/classifier_assets_metadata.json";
77     private static final String PRODUCTION_CLASSIFIER_INPUT_CONFIG_PATH =
78             "classifier/classifier_input_config.txt";
79     private static final String BUNDLED_MODEL_FILE_PATH = "classifier/model.tflite";
80 
81     private ImmutableList<Integer> testLabels;
82     private ImmutableMap<String, ImmutableMap<String, String>> testClassifierAssetsMetadata;
83     private long mTestTaxonomyVersion;
84     private long mTestModelVersion;
85 
86     private ImmutableList<Integer> productionLabels;
87     private ImmutableMap<String, ImmutableMap<String, String>> productionClassifierAssetsMetadata;
88     private long mProductionTaxonomyVersion;
89     private long mProductionModelVersion;
90 
91     @Mock private SynchronousFileStorage mMockFileStorage;
92     @Mock private Map<String, ClientFile> mMockDownloadedFiles;
93     @Mock private AdServicesLogger mLogger;
94 
95     @Before
setUp()96     public void setUp() {
97         mocker.mockGetFlagsForTesting();
98 
99         ModelManager testModelManager =
100                 new ModelManager(
101                         mContext,
102                         TEST_LABELS_FILE_PATH,
103                         TEST_PRECOMPUTED_FILE_PATH,
104                         TEST_CLASSIFIER_ASSETS_METADATA_PATH,
105                         TEST_CLASSIFIER_INPUT_CONFIG_PATH,
106                         BUNDLED_MODEL_FILE_PATH,
107                         mMockFileStorage,
108                         mMockDownloadedFiles);
109 
110         ModelManager productionModelManager =
111                 new ModelManager(
112                         mContext,
113                         PRODUCTION_LABELS_FILE_PATH,
114                         PRODUCTION_APPS_FILE_PATH,
115                         PRODUCTION_CLASSIFIER_ASSETS_METADATA_PATH,
116                         PRODUCTION_CLASSIFIER_INPUT_CONFIG_PATH,
117                         BUNDLED_MODEL_FILE_PATH,
118                         mMockFileStorage,
119                         mMockDownloadedFiles);
120 
121         // TODO (b/359964245): Delete after bug is resolved and use annotations to verify calls.
122         doNothingOnErrorLogUtilError();
123 
124         testLabels = testModelManager.retrieveLabels();
125         testClassifierAssetsMetadata = testModelManager.retrieveClassifierAssetsMetadata();
126         mTestTaxonomyVersion =
127                 Long.parseLong(
128                         testClassifierAssetsMetadata.get("labels_topics").get("asset_version"));
129         mTestModelVersion =
130                 Long.parseLong(
131                         testClassifierAssetsMetadata.get("tflite_model").get("asset_version"));
132 
133         productionLabels = productionModelManager.retrieveLabels();
134         productionClassifierAssetsMetadata =
135                 productionModelManager.retrieveClassifierAssetsMetadata();
136         mProductionTaxonomyVersion =
137                 Long.parseLong(
138                         productionClassifierAssetsMetadata
139                                 .get("labels_topics")
140                                 .get("asset_version"));
141         mProductionModelVersion =
142                 Long.parseLong(
143                         productionClassifierAssetsMetadata
144                                 .get("tflite_model")
145                                 .get("asset_version"));
146     }
147 
148     @Test
testGetTopTopics_legalInput()149     public void testGetTopTopics_legalInput() {
150         ArgumentCaptor<EpochComputationGetTopTopicsStats> argument =
151                 ArgumentCaptor.forClass(EpochComputationGetTopTopicsStats.class);
152         // construction the appTopics map so that when sorting by the number of occurrences,
153         // the order of topics are:
154         // topic1, topic2, topic3, topic4, topic5, ...,
155         Map<String, List<Topic>> appTopics = new HashMap<>();
156         appTopics.put("app1", getTestTopics(Arrays.asList(1, 2, 3, 4, 5)));
157         appTopics.put("app2", getTestTopics(Arrays.asList(1, 2, 3, 4, 5)));
158         appTopics.put("app3", getTestTopics(Arrays.asList(1, 2, 3, 4, 16)));
159         appTopics.put("app4", getTestTopics(Arrays.asList(1, 2, 3, 13, 17)));
160         appTopics.put("app5", getTestTopics(Arrays.asList(1, 2, 11, 14, 18)));
161         appTopics.put("app6", getTestTopics(Arrays.asList(1, 10, 12, 15, 19)));
162 
163         // This test case should return top 5 topics from appTopics and 1 random topic
164         List<Topic> testResponse =
165                 getTopTopics(
166                         appTopics,
167                         testLabels,
168                         new Random(),
169                         /* numberOfTopTopics */ 5,
170                         /* numberOfRandomTopics */ 1,
171                         mLogger);
172 
173         assertThat(testResponse).hasSize(6);
174         expect.that(testResponse.get(0)).isEqualTo(getTestTopic(1));
175         expect.that(testResponse.get(1)).isEqualTo(getTestTopic(2));
176         expect.that(testResponse.get(2)).isEqualTo(getTestTopic(3));
177         expect.that(testResponse.get(3)).isEqualTo(getTestTopic(4));
178         expect.that(testResponse.get(4)).isEqualTo(getTestTopic(5));
179         // Check the random topic is not empty
180         // The random topic is at the end
181         expect.that(testResponse.get(5)).isNotNull();
182 
183         verify(mLogger).logEpochComputationGetTopTopicsStats(argument.capture());
184         expect.that(argument.getValue())
185                 .isEqualTo(
186                         EpochComputationGetTopTopicsStats.builder()
187                                 .setTopTopicCount(5)
188                                 .setPaddedRandomTopicsCount(0)
189                                 .setAppsConsideredCount(6)
190                                 .setSdksConsideredCount(-1)
191                                 .build());
192     }
193 
194     @Test
testGetTopTopics_largeTopTopicsInput()195     public void testGetTopTopics_largeTopTopicsInput() {
196         ArgumentCaptor<EpochComputationGetTopTopicsStats> argument =
197                 ArgumentCaptor.forClass(EpochComputationGetTopTopicsStats.class);
198 
199         Map<String, List<Topic>> appTopics = new HashMap<>();
200         appTopics.put("app1", getTestTopics(Arrays.asList(1, 2, 3, 4, 5)));
201 
202         // We only have 5 topics but requesting for 15 topics,
203         // so we will pad them with 10 random topics.
204         List<Topic> testResponse =
205                 getTopTopics(
206                         appTopics,
207                         testLabels,
208                         new Random(),
209                         /* numberOfTopTopics */ 15,
210                         /* numberOfRandomTopics */ 1,
211                         mLogger);
212 
213         // The response body should contain 11 topics.
214         assertThat(testResponse.size()).isEqualTo(16);
215         verify(mLogger).logEpochComputationGetTopTopicsStats(argument.capture());
216         assertThat(argument.getValue())
217                 .isEqualTo(
218                         EpochComputationGetTopTopicsStats.builder()
219                                 .setTopTopicCount(15)
220                                 .setPaddedRandomTopicsCount(10)
221                                 .setAppsConsideredCount(1)
222                                 .setSdksConsideredCount(-1)
223                                 .build());
224     }
225 
226     @Test
testGetTopTopics_zeroTopTopics()227     public void testGetTopTopics_zeroTopTopics() {
228         Map<String, List<Topic>> appTopics = new HashMap<>();
229         appTopics.put("app1", getTestTopics(Arrays.asList(1, 2, 3, 4, 5)));
230 
231         // This test case should throw an IllegalArgumentException if numberOfTopTopics is 0.
232         assertThrows(
233                 IllegalArgumentException.class,
234                 () ->
235                         getTopTopics(
236                                 appTopics,
237                                 testLabels,
238                                 new Random(),
239                                 /* numberOfTopTopics */ 0,
240                                 /* numberOfRandomTopics */ 1,
241                                 mLogger));
242     }
243 
244     @Test
testGetTopTopics_zeroRandomTopics()245     public void testGetTopTopics_zeroRandomTopics() {
246         Map<String, List<Topic>> appTopics = new HashMap<>();
247         appTopics.put("app1", getTestTopics(Arrays.asList(1, 2, 3, 4, 5)));
248         // This test case should throw an IllegalArgumentException if numberOfRandomTopics is 0.
249         assertThrows(
250                 IllegalArgumentException.class,
251                 () ->
252                         getTopTopics(
253                                 appTopics,
254                                 testLabels,
255                                 new Random(),
256                                 /* numberOfTopTopics */ 3,
257                                 /* numberOfRandomTopics */ 0,
258                                 mLogger));
259     }
260 
261     @Test
testGetTopTopics_negativeTopTopics()262     public void testGetTopTopics_negativeTopTopics() {
263         Map<String, List<Topic>> appTopics = new HashMap<>();
264         appTopics.put("app1", getTestTopics(Arrays.asList(1, 2, 3, 4, 5)));
265 
266         // This test case should throw an IllegalArgumentException if numberOfTopTopics is negative.
267         assertThrows(
268                 IllegalArgumentException.class,
269                 () ->
270                         getTopTopics(
271                                 appTopics,
272                                 testLabels,
273                                 new Random(),
274                                 /* numberOfTopTopics */ -5,
275                                 /* numberOfRandomTopics */ 1,
276                                 mLogger));
277     }
278 
279     @Test
testGetTopTopics_negativeRandomTopics()280     public void testGetTopTopics_negativeRandomTopics() {
281         Map<String, List<Topic>> appTopics = new HashMap<>();
282         appTopics.put("app1", getTestTopics(Arrays.asList(1, 2, 3, 4, 5)));
283 
284         // This test case should throw an IllegalArgumentException
285         // if numberOfRandomTopics is negative.
286         assertThrows(
287                 IllegalArgumentException.class,
288                 () ->
289                         getTopTopics(
290                                 appTopics,
291                                 testLabels,
292                                 new Random(),
293                                 /* numberOfTopTopics */ 3,
294                                 /* numberOfRandomTopics */ -1,
295                                 mLogger));
296     }
297 
298     @Test
testGetTopTopics_emptyAppTopicsMap()299     public void testGetTopTopics_emptyAppTopicsMap() {
300         Map<String, List<Topic>> appTopics = new HashMap<>();
301 
302         // The device does not have an app, an empty top topics list should be returned.
303         List<Topic> testResponse =
304                 getTopTopics(
305                         appTopics,
306                         testLabels,
307                         new Random(),
308                         /* numberOfTopTopics */ 5,
309                         /* numberOfRandomTopics */ 1,
310                         mLogger);
311 
312         // The response body should be empty.
313         assertThat(testResponse).isEmpty();
314     }
315 
316     @Test
testGetTopTopics_emptyTopicInEachApp()317     public void testGetTopTopics_emptyTopicInEachApp() {
318         ArgumentCaptor<EpochComputationGetTopTopicsStats> argument =
319                 ArgumentCaptor.forClass(EpochComputationGetTopTopicsStats.class);
320         Map<String, List<Topic>> appTopics = new HashMap<>();
321 
322         // app1 and app2 do not have any classification topics.
323         appTopics.put("app1", new ArrayList<>());
324         appTopics.put("app2", new ArrayList<>());
325 
326         // The device have some apps but the topic corresponding to the app cannot be obtained.
327         // In this test case, an empty top topics list should be returned.
328         List<Topic> testResponse =
329                 getTopTopics(
330                         appTopics,
331                         testLabels,
332                         new Random(),
333                         /* numberOfTopTopics */ 5,
334                         /* numberOfRandomTopics */ 1,
335                         mLogger);
336 
337         // The response body should be empty
338         assertThat(testResponse).isEmpty();
339         verify(mLogger).logEpochComputationGetTopTopicsStats(argument.capture());
340         assertThat(argument.getValue())
341                 .isEqualTo(
342                         EpochComputationGetTopTopicsStats.builder()
343                                 .setTopTopicCount(0)
344                                 .setPaddedRandomTopicsCount(0)
345                                 .setAppsConsideredCount(2)
346                                 .setSdksConsideredCount(-1)
347                                 .build());
348     }
349 
350     @Test
testGetTopTopics_selectSingleRandomTopic()351     public void testGetTopTopics_selectSingleRandomTopic() {
352         // In this test, in order to make test result to be deterministic so CommonClassifierHelper
353         // has to be mocked to get a random topic. However, real CommonClassifierHelper need to
354         // be tested as well. Therefore, real methods will be called for the other top topics.
355         //
356         // Initialize MockRandom. Append 3 random positive integers (20, 100, 300) to MockRandom
357         // array,
358         // their corresponding topicIds in the topics list will not overlap with
359         // the topicIds of app1 below.
360         MockRandom mockRandom = new MockRandom(new long[] {20, 100, 300});
361 
362         Map<String, List<Topic>> testAppTopics = new HashMap<>();
363         // We label app1 with the first 5 topics in topics list.
364         testAppTopics.put("app1", getTestTopics(Arrays.asList(253, 146, 277, 59, 127)));
365 
366         // Test the random topic with labels file in test assets.
367         List<Topic> testResponse =
368                 getTopTopics(
369                         testAppTopics,
370                         testLabels,
371                         mockRandom,
372                         /* numberOfTopTopics */ 5,
373                         /* numberOfRandomTopics */ 1,
374                         mLogger);
375 
376         // The response body should contain 5 topics + 1 random topic.
377         assertThat(testResponse.size()).isEqualTo(6);
378 
379         // In the following test, we need to verify that the mock random integer index
380         // can match the correct topic in classifier/precomputed_test_app_list_chrome_topics.csv.
381         // "random = n, topicId = m" means this topicId m is from the nth (0-indexed)
382         // topicId in the topics list.
383         // random = 20, topicId = 10021
384         assertThat(testResponse.get(5)).isEqualTo(getTestTopic(10021));
385 
386         Map<String, List<Topic>> productionAppTopics = new HashMap<>();
387         // We label app1 with the same topic IDs as testAppTopics, but using production metadata.
388         productionAppTopics.put("app1", getProductionTopics(Arrays.asList(253, 146, 277, 59, 127)));
389 
390         // Test the random topic with labels file in production assets.
391         List<Topic> productionResponse =
392                 getTopTopics(
393                         productionAppTopics,
394                         productionLabels,
395                         new MockRandom(new long[] {50, 100, 300}),
396                         /* numberOfTopTopics */ 5,
397                         /* numberOfRandomTopics */ 1,
398                         mLogger);
399 
400         // The response body should contain 5 topics + 1 random topic.
401         assertThat(productionResponse.size()).isEqualTo(6);
402 
403         // In the following test, we need to verify that the mock random integer index
404         // can match the correct topic in classifier/precomputed_app_list_chrome_topics.csv.
405         // "random = n, topicId = m" means this topicId m is from the nth (0-indexed)
406         // topicId in the topics list.
407         // random = 50, topicId = 10051
408         assertThat(productionResponse.get(5)).isEqualTo(getProductionTopic(10051));
409     }
410 
411     @Test
testGetTopTopics_selectMultipleRandomTopic()412     public void testGetTopTopics_selectMultipleRandomTopic() {
413         // In this test, in order to make test result to be deterministic so CommonClassifierHelper
414         // has to be mocked to get some random topics. However, real CommonClassifierHelper need to
415         // be tested as well. Therefore, real methods will be called for the other top topics.
416         //
417         // Initialize MockRandom. Randomly select 7 indices in MockRandom, their corresponding
418         // topicIds in the topics list
419         // will not overlap with the topicIds of app1 below. 500 in MockRandom exceeds the length
420         // of topics list, so what it represents should be 151st (500 % 349 = 151) topicId
421         // in topics list.
422         MockRandom mockRandom = new MockRandom(new long[] {10, 20, 50, 75, 100, 300, 500});
423 
424         Map<String, List<Topic>> appTopics = new HashMap<>();
425         // The topicId we use is verticals4 and its index range is from 0 to 1918.
426         // We label app1 with the first 5 topicIds in topics list.
427         appTopics.put("app1", getTestTopics(Arrays.asList(34, 89, 69, 349, 241)));
428 
429         List<Topic> testResponse =
430                 getTopTopics(
431                         appTopics,
432                         testLabels,
433                         mockRandom,
434                         /* numberOfTopTopics */ 5,
435                         /* numberOfRandomTopics */ 7,
436                         mLogger);
437 
438         // The response body should contain 5 topics + 7 random topic.
439         assertThat(testResponse.size()).isEqualTo(12);
440 
441         // In the following tests, we need to verify that the mock random integer index
442         // can match the correct topic in classifier/precomputed_test_app_list_chrome_topics.csv.
443         // "random = n, topicId = m" means this topicId m is from the nth (0-indexed)
444         // topicId in the topics list.
445         // random = 10, topicId = 10011
446         assertThat(testResponse.get(5)).isEqualTo(getTestTopic(10011));
447 
448         // random = 20, topicId = 10021
449         assertThat(testResponse.get(6)).isEqualTo(getTestTopic(10021));
450 
451         // random = 50, topicId = 10051
452         assertThat(testResponse.get(7)).isEqualTo(getTestTopic(10051));
453 
454         // random = 75, topicId = 10076
455         assertThat(testResponse.get(8)).isEqualTo(getTestTopic(10076));
456 
457         // random = 100, topicId = 10101
458         assertThat(testResponse.get(9)).isEqualTo(getTestTopic(10101));
459 
460         // random = 300, topicId = 10301
461         assertThat(testResponse.get(10)).isEqualTo(getTestTopic(10301));
462 
463         // random = 500, size of labels list is 446,
464         // index should be 500 % 446 = 54, topicId = 10055
465         assertThat(testResponse.get(11)).isEqualTo(getTestTopic(10055));
466     }
467 
468     @Test
testGetTopTopics_selectDuplicateRandomTopic()469     public void testGetTopTopics_selectDuplicateRandomTopic() {
470         ArgumentCaptor<EpochComputationGetTopTopicsStats> argument =
471                 ArgumentCaptor.forClass(EpochComputationGetTopTopicsStats.class);
472         // In this test, in order to make test result to be deterministic so CommonClassifierHelper
473         // has to be mocked to get a random topic. However, real CommonClassifierHelper need to
474         // be tested as well. Therefore, real methods will be called for the other top topics.
475         //
476         // Initialize MockRandom. Randomly select 6 indices in MockRandom, their first 5
477         // corresponding topicIds
478         // in the topics list will overlap with the topicIds of app1 below.
479         MockRandom mockRandom = new MockRandom(new long[] {1, 5, 10, 25, 100, 300});
480 
481         Map<String, List<Topic>> appTopics = new HashMap<>();
482 
483         // If the random topic duplicates with the real topic, then pick another random
484         // one until no duplicates. In this test, we will let app1 have five topicIds of
485         // 2, 6, 11, 26, 101. These topicIds are the same as the topicIds in the
486         // classifier/precomputed_test_app_list_chrome_topics.csv corresponding to
487         // the first five indices in the MockRandomArray.
488         appTopics.put("app1", getTestTopics(Arrays.asList(2, 6, 11, 26, 101)));
489 
490         List<Topic> testResponse =
491                 getTopTopics(
492                         appTopics,
493                         testLabels,
494                         mockRandom,
495                         /* numberOfTopTopics */ 5,
496                         /* numberOfRandomTopics */ 1,
497                         mLogger);
498 
499         // The response body should contain 5 topics + 1 random topic
500         assertThat(testResponse.size()).isEqualTo(6);
501 
502         // In the following tests, we need to verify that the mock random integer index
503         // can match the correct topic in classifier/precomputed_test_app_list_chrome_topics.csv.
504         // "random = n, topicId = m" means this topicId m is from the nth (0-indexed)
505         // topicId in the topics list.
506         // In this test, if we want to select a random topic that does not repeat,
507         // we should select the one corresponding to the sixth index
508         // in the MockRandom array topicId, i.e. random = 1, topicId = 10002
509         assertThat(testResponse.get(5)).isEqualTo(getTestTopic(10002));
510         verify(mLogger).logEpochComputationGetTopTopicsStats(argument.capture());
511         assertThat(argument.getValue())
512                 .isEqualTo(
513                         EpochComputationGetTopTopicsStats.builder()
514                                 .setTopTopicCount(5)
515                                 .setPaddedRandomTopicsCount(0)
516                                 .setAppsConsideredCount(1)
517                                 .setSdksConsideredCount(-1)
518                                 .build());
519     }
520 
521     @Test
testComputeTestAssetChecksum()522     public void testComputeTestAssetChecksum() {
523         // Compute SHA256 checksum of labels topics file in test assets and check the result
524         // can match the checksum saved in the test classifier assets metadata file.
525         String labelsTestTopicsChecksum =
526                 computeClassifierAssetChecksum(mContext.getAssets(), TEST_LABELS_FILE_PATH);
527         assertThat(labelsTestTopicsChecksum)
528                 .isEqualTo(testClassifierAssetsMetadata.get("labels_topics").get("checksum"));
529 
530         // Compute SHA256 checksum of precomputed apps topics file in test assets
531         // and check the result can match the checksum saved in the classifier assets metadata file.
532         String precomputedAppsTestChecksum =
533                 computeClassifierAssetChecksum(mContext.getAssets(), TEST_PRECOMPUTED_FILE_PATH);
534         assertThat(precomputedAppsTestChecksum)
535                 .isEqualTo(
536                         testClassifierAssetsMetadata.get("precomputed_app_list").get("checksum"));
537     }
538 
539     @Test
testComputeProductionAssetChecksum()540     public void testComputeProductionAssetChecksum() {
541         // Compute SHA256 checksum of labels topics file in production assets and check the result
542         // can match the checksum saved in the production classifier assets metadata file.
543         String labelsProductionTopicsChecksum =
544                 computeClassifierAssetChecksum(mContext.getAssets(), PRODUCTION_LABELS_FILE_PATH);
545         assertThat(labelsProductionTopicsChecksum)
546                 .isEqualTo(productionClassifierAssetsMetadata.get("labels_topics").get("checksum"));
547 
548         // Compute SHA256 checksum of precomputed apps topics file in production assets
549         // and check the result can match the checksum saved in the classifier assets metadata file.
550         String precomputedAppsProductionChecksum =
551                 computeClassifierAssetChecksum(mContext.getAssets(), PRODUCTION_APPS_FILE_PATH);
552         assertThat(precomputedAppsProductionChecksum)
553                 .isEqualTo(
554                         productionClassifierAssetsMetadata
555                                 .get("precomputed_app_list")
556                                 .get("checksum"));
557     }
558 
559     @Test
testGetBundledModelBuildId()560     public void testGetBundledModelBuildId() {
561         // Verify bundled model build_id. This should be changed along with model update.
562         assertThat(
563                         CommonClassifierHelper.getBundledModelBuildId(
564                                 mContext, PRODUCTION_CLASSIFIER_ASSETS_METADATA_PATH))
565                 .isEqualTo(1986);
566         // Verify test model build_id.
567         assertThat(
568                         CommonClassifierHelper.getBundledModelBuildId(
569                                 mContext, TEST_CLASSIFIER_ASSETS_METADATA_PATH))
570                 .isEqualTo(8);
571     }
572 
getTestTopic(int topicId)573     private Topic getTestTopic(int topicId) {
574         return Topic.create(topicId, mTestTaxonomyVersion, mTestModelVersion);
575     }
576 
getTestTopics(List<Integer> topicIds)577     private List<Topic> getTestTopics(List<Integer> topicIds) {
578         return topicIds.stream().map(this::getTestTopic).collect(Collectors.toList());
579     }
580 
getProductionTopic(int topicId)581     private Topic getProductionTopic(int topicId) {
582         return Topic.create(topicId, mProductionTaxonomyVersion, mProductionModelVersion);
583     }
584 
getProductionTopics(List<Integer> topicIds)585     private List<Topic> getProductionTopics(List<Integer> topicIds) {
586         return topicIds.stream().map(this::getProductionTopic).collect(Collectors.toList());
587     }
588 }
589