1 /*
2  * Copyright (C) 2021 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.car.telemetry;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import android.car.telemetry.MetricsConfigKey;
23 import android.os.PersistableBundle;
24 
25 import org.junit.Before;
26 import org.junit.Test;
27 import org.junit.runner.RunWith;
28 import org.mockito.junit.MockitoJUnitRunner;
29 
30 import java.io.ByteArrayOutputStream;
31 import java.io.File;
32 import java.io.FileInputStream;
33 import java.nio.charset.StandardCharsets;
34 import java.nio.file.Files;
35 import java.util.concurrent.TimeUnit;
36 
37 @RunWith(MockitoJUnitRunner.class)
38 public class ResultStoreTest {
39     private static final PersistableBundle TEST_INTERIM_BUNDLE = new PersistableBundle();
40     private static final PersistableBundle TEST_FINAL_BUNDLE = new PersistableBundle();
41     private static final TelemetryProto.TelemetryError TEST_TELEMETRY_ERROR =
42             TelemetryProto.TelemetryError.newBuilder().setMessage("test error").build();
43 
44     private File mTestRootDir;
45     private File mTestInterimResultDir;
46     private File mTestErrorResultDir;
47     private File mTestFinalResultDir;
48     private ResultStore mResultStore;
49 
50     @Before
setUp()51     public void setUp() throws Exception {
52         TEST_INTERIM_BUNDLE.putString("test key", "interim value");
53         TEST_FINAL_BUNDLE.putString("test key", "final value");
54 
55         mTestRootDir = Files.createTempDirectory("car_telemetry_test").toFile();
56         mTestInterimResultDir = new File(mTestRootDir, ResultStore.INTERIM_RESULT_DIR);
57         mTestErrorResultDir = new File(mTestRootDir, ResultStore.ERROR_RESULT_DIR);
58         mTestFinalResultDir = new File(mTestRootDir, ResultStore.FINAL_RESULT_DIR);
59 
60         mResultStore = new ResultStore(mTestRootDir);
61     }
62 
63     @Test
testConstructor_shouldCreateResultsFolder()64     public void testConstructor_shouldCreateResultsFolder() {
65         // constructor is called in setUp()
66         assertThat(mTestInterimResultDir.exists()).isTrue();
67         assertThat(mTestFinalResultDir.exists()).isTrue();
68     }
69 
70     @Test
testConstructor_shouldLoadInterimResultsIntoMemory()71     public void testConstructor_shouldLoadInterimResultsIntoMemory() throws Exception {
72         String testInterimFileName = "test_file_1";
73         writeBundleToFile(mTestInterimResultDir, testInterimFileName, TEST_INTERIM_BUNDLE);
74 
75         mResultStore = new ResultStore(mTestRootDir);
76 
77         // should compare value instead of reference
78         assertThat(mResultStore.getInterimResult(testInterimFileName).toString())
79                 .isEqualTo(TEST_INTERIM_BUNDLE.toString());
80     }
81 
82     @Test
testFlushToDisk_shouldRemoveStaleData()83     public void testFlushToDisk_shouldRemoveStaleData() throws Exception {
84         File staleTestFile1 = new File(mTestInterimResultDir, "stale_test_file_1");
85         File staleTestFile2 = new File(mTestFinalResultDir, "stale_test_file_2");
86         File activeTestFile3 = new File(mTestInterimResultDir, "active_test_file_3");
87         writeBundleToFile(staleTestFile1, TEST_INTERIM_BUNDLE);
88         writeBundleToFile(staleTestFile2, TEST_FINAL_BUNDLE);
89         writeBundleToFile(activeTestFile3, TEST_INTERIM_BUNDLE);
90         long currTimeMs = System.currentTimeMillis();
91         staleTestFile1.setLastModified(0L); // stale
92         staleTestFile2.setLastModified(
93                 currTimeMs - TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS)); // stale
94         activeTestFile3.setLastModified(
95                 currTimeMs - TimeUnit.MILLISECONDS.convert(29, TimeUnit.DAYS)); // active
96 
97         mResultStore.flushToDisk();
98 
99         assertThat(staleTestFile1.exists()).isFalse();
100         assertThat(staleTestFile2.exists()).isFalse();
101         assertThat(activeTestFile3.exists()).isTrue();
102     }
103 
104 
105     @Test
testPutInterimResultAndFlushToDisk_shouldReplaceExistingFile()106     public void testPutInterimResultAndFlushToDisk_shouldReplaceExistingFile() throws Exception {
107         String newKey = "new key";
108         String newValue = "new value";
109         String metricsConfigName = "my_metrics_config";
110         writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
111         TEST_INTERIM_BUNDLE.putString(newKey, newValue);
112 
113         mResultStore.putInterimResult(metricsConfigName, TEST_INTERIM_BUNDLE);
114         mResultStore.flushToDisk();
115 
116         PersistableBundle bundle = readBundleFromFile(mTestInterimResultDir, metricsConfigName);
117         assertThat(bundle.getString(newKey)).isEqualTo(newValue);
118         assertThat(bundle.toString()).isEqualTo(TEST_INTERIM_BUNDLE.toString());
119     }
120 
121     @Test
testPutInterimResultAndFlushToDisk_shouldWriteDirtyResultsOnly()122     public void testPutInterimResultAndFlushToDisk_shouldWriteDirtyResultsOnly() throws Exception {
123         File fileFoo = new File(mTestInterimResultDir, "foo");
124         File fileBar = new File(mTestInterimResultDir, "bar");
125         writeBundleToFile(fileFoo, TEST_INTERIM_BUNDLE);
126         writeBundleToFile(fileBar, TEST_INTERIM_BUNDLE);
127         mResultStore = new ResultStore(mTestRootDir); // re-load data
128         PersistableBundle newData = new PersistableBundle();
129         newData.putDouble("pi", 3.1415926);
130 
131         mResultStore.putInterimResult("bar", newData); // make bar dirty
132         fileFoo.delete(); // delete the clean file from the file system
133         mResultStore.flushToDisk(); // write dirty data
134 
135         // foo is a clean file that should not be written in flushToDisk()
136         assertThat(fileFoo.exists()).isFalse();
137         assertThat(readBundleFromFile(fileBar).toString()).isEqualTo(newData.toString());
138     }
139 
140     @Test
testGetFinalResult_whenNoData_shouldReceiveNull()141     public void testGetFinalResult_whenNoData_shouldReceiveNull() throws Exception {
142         String metricsConfigName = "my_metrics_config";
143 
144         PersistableBundle bundle = mResultStore.getFinalResult(metricsConfigName, true);
145 
146         assertThat(bundle).isNull();
147     }
148 
149     @Test
testGetFinalResult_whenDataCorrupt_shouldReceiveNull()150     public void testGetFinalResult_whenDataCorrupt_shouldReceiveNull() throws Exception {
151         String metricsConfigName = "my_metrics_config";
152         Files.write(new File(mTestFinalResultDir, metricsConfigName).toPath(),
153                 "not a bundle".getBytes(StandardCharsets.UTF_8));
154 
155         PersistableBundle bundle = mResultStore.getFinalResult(metricsConfigName, true);
156 
157         assertThat(bundle).isNull();
158     }
159 
160     @Test
testGetFinalResult_whenDeleteFlagTrue_shouldDeleteData()161     public void testGetFinalResult_whenDeleteFlagTrue_shouldDeleteData() throws Exception {
162         String testFinalFileName = "my_metrics_config";
163         writeBundleToFile(mTestFinalResultDir, testFinalFileName, TEST_FINAL_BUNDLE);
164 
165         PersistableBundle bundle = mResultStore.getFinalResult(testFinalFileName, true);
166 
167         // should compare value instead of reference
168         assertThat(bundle.toString()).isEqualTo(TEST_FINAL_BUNDLE.toString());
169         assertThat(new File(mTestFinalResultDir, testFinalFileName).exists()).isFalse();
170     }
171 
172     @Test
testGetFinalResult_whenDeleteFlagFalse_shouldNotDeleteData()173     public void testGetFinalResult_whenDeleteFlagFalse_shouldNotDeleteData() throws Exception {
174         String testFinalFileName = "my_metrics_config";
175         writeBundleToFile(mTestFinalResultDir, testFinalFileName, TEST_FINAL_BUNDLE);
176 
177         PersistableBundle bundle = mResultStore.getFinalResult(testFinalFileName, false);
178 
179         // should compare value instead of reference
180         assertThat(bundle.toString()).isEqualTo(TEST_FINAL_BUNDLE.toString());
181         assertThat(new File(mTestFinalResultDir, testFinalFileName).exists()).isTrue();
182     }
183 
184     @Test
testGetError_whenNoError_shouldReceiveNull()185     public void testGetError_whenNoError_shouldReceiveNull() {
186         String metricsConfigName = "my_metrics_config";
187 
188         TelemetryProto.TelemetryError error = mResultStore.getError(metricsConfigName, true);
189 
190         assertThat(error).isNull();
191     }
192 
193     @Test
testGetError_shouldReceiveError()194     public void testGetError_shouldReceiveError() throws Exception {
195         String metricsConfigName = "my_metrics_config";
196         // write serialized error object to file
197         Files.write(
198                 new File(mTestErrorResultDir, metricsConfigName).toPath(),
199                 TEST_TELEMETRY_ERROR.toByteArray());
200 
201         TelemetryProto.TelemetryError error = mResultStore.getError(metricsConfigName, true);
202 
203         assertThat(error).isEqualTo(TEST_TELEMETRY_ERROR);
204     }
205 
206     @Test
testPutFinalResult_shouldWriteResultAndRemoveInterim()207     public void testPutFinalResult_shouldWriteResultAndRemoveInterim() throws Exception {
208         String metricsConfigName = "my_metrics_config";
209         writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
210 
211         mResultStore.putFinalResult(metricsConfigName, TEST_FINAL_BUNDLE);
212 
213         assertThat(mResultStore.getInterimResult(metricsConfigName)).isNull();
214         assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
215         assertThat(new File(mTestFinalResultDir, metricsConfigName).exists()).isTrue();
216     }
217 
218     @Test
testPutError_shouldWriteErrorAndRemoveInterimResultFile()219     public void testPutError_shouldWriteErrorAndRemoveInterimResultFile() throws Exception {
220         String metricsConfigName = "my_metrics_config";
221         writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
222 
223         mResultStore.putError(metricsConfigName, TEST_TELEMETRY_ERROR);
224 
225         assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
226         assertThat(new File(mTestErrorResultDir, metricsConfigName).exists()).isTrue();
227     }
228 
229     @Test
testRemoveResult_whenInterimResult_shouldDelete()230     public void testRemoveResult_whenInterimResult_shouldDelete() throws Exception {
231         String metricsConfigName = "my_metrics_config";
232         MetricsConfigKey key = new MetricsConfigKey(metricsConfigName, 1);
233         writeBundleToFile(mTestInterimResultDir, metricsConfigName, TEST_INTERIM_BUNDLE);
234 
235         mResultStore.removeResult(key);
236 
237         assertThat(new File(mTestInterimResultDir, metricsConfigName).exists()).isFalse();
238     }
239 
240     @Test
testRemoveResult_whenFinalResult_shouldDelete()241     public void testRemoveResult_whenFinalResult_shouldDelete() throws Exception {
242         String metricsConfigName = "my_metrics_config";
243         MetricsConfigKey key = new MetricsConfigKey(metricsConfigName, 1);
244         writeBundleToFile(mTestFinalResultDir, metricsConfigName, TEST_FINAL_BUNDLE);
245 
246         mResultStore.removeResult(key);
247 
248         assertThat(new File(mTestFinalResultDir, metricsConfigName).exists()).isFalse();
249     }
250 
251     @Test
testRemoveAllResults_shouldDeleteAll()252     public void testRemoveAllResults_shouldDeleteAll() throws Exception {
253         mResultStore.putInterimResult("config 1", TEST_INTERIM_BUNDLE);
254         mResultStore.putFinalResult("config 2", TEST_FINAL_BUNDLE);
255         mResultStore.putError("config 3", TEST_TELEMETRY_ERROR);
256         mResultStore.flushToDisk();
257 
258         mResultStore.removeAllResults();
259 
260         assertThat(mTestInterimResultDir.listFiles()).isEmpty();
261         assertThat(mTestFinalResultDir.listFiles()).isEmpty();
262     }
263 
writeBundleToFile( File dir, String fileName, PersistableBundle persistableBundle)264     private void writeBundleToFile(
265             File dir, String fileName, PersistableBundle persistableBundle) throws Exception {
266         writeBundleToFile(new File(dir, fileName), persistableBundle);
267     }
268 
269     /**
270      * Writes a persistable bundle to the result directory with the given directory and file name,
271      * and verifies that it was successfully written.
272      */
writeBundleToFile( File file, PersistableBundle persistableBundle)273     private void writeBundleToFile(
274             File file, PersistableBundle persistableBundle) throws Exception {
275         try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
276             persistableBundle.writeToStream(byteArrayOutputStream);
277             Files.write(file.toPath(), byteArrayOutputStream.toByteArray());
278         }
279         assertWithMessage("bundle is not written to the result directory")
280                 .that(file.exists()).isTrue();
281     }
282 
readBundleFromFile(File dir, String fileName)283     private PersistableBundle readBundleFromFile(File dir, String fileName) throws Exception {
284         return readBundleFromFile(new File(dir, fileName));
285     }
286 
287     /** Reads a persistable bundle from the given path. */
readBundleFromFile(File file)288     private PersistableBundle readBundleFromFile(File file) throws Exception {
289         try (FileInputStream fis = new FileInputStream(file)) {
290             return PersistableBundle.readFromStream(fis);
291         }
292     }
293 }
294