1/*
2 * Copyright (C) 2012 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 com.android.internal.util;
18
19import static android.text.format.DateUtils.DAY_IN_MILLIS;
20import static android.text.format.DateUtils.HOUR_IN_MILLIS;
21import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
22import static android.text.format.DateUtils.SECOND_IN_MILLIS;
23import static android.text.format.DateUtils.WEEK_IN_MILLIS;
24import static android.text.format.DateUtils.YEAR_IN_MILLIS;
25
26import android.test.AndroidTestCase;
27import android.test.suitebuilder.annotation.Suppress;
28import android.util.Log;
29
30import com.android.internal.util.FileRotator.Reader;
31import com.android.internal.util.FileRotator.Writer;
32import com.google.android.collect.Lists;
33
34import java.io.DataInputStream;
35import java.io.DataOutputStream;
36import java.io.File;
37import java.io.FileOutputStream;
38import java.io.IOException;
39import java.io.InputStream;
40import java.io.OutputStream;
41import java.net.ProtocolException;
42import java.util.ArrayList;
43import java.util.Arrays;
44import java.util.Random;
45
46import libcore.io.IoUtils;
47
48/**
49 * Tests for {@link FileRotator}.
50 */
51public class FileRotatorTest extends AndroidTestCase {
52    private static final String TAG = "FileRotatorTest";
53
54    private File mBasePath;
55
56    private static final String PREFIX = "rotator";
57    private static final String ANOTHER_PREFIX = "another_rotator";
58
59    private static final long TEST_TIME = 1300000000000L;
60
61    // TODO: test throwing rolls back correctly
62
63    @Override
64    protected void setUp() throws Exception {
65        super.setUp();
66
67        mBasePath = getContext().getFilesDir();
68        IoUtils.deleteContents(mBasePath);
69    }
70
71    public void testEmpty() throws Exception {
72        final FileRotator rotate1 = new FileRotator(
73                mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS);
74        final FileRotator rotate2 = new FileRotator(
75                mBasePath, ANOTHER_PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS);
76
77        final RecordingReader reader = new RecordingReader();
78        long currentTime = TEST_TIME;
79
80        // write single new value
81        rotate1.combineActive(reader, writer("foo"), currentTime);
82        reader.assertRead();
83
84        // assert that one rotator doesn't leak into another
85        assertReadAll(rotate1, "foo");
86        assertReadAll(rotate2);
87    }
88
89    public void testCombine() throws Exception {
90        final FileRotator rotate = new FileRotator(
91                mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS);
92
93        final RecordingReader reader = new RecordingReader();
94        long currentTime = TEST_TIME;
95
96        // first combine should have empty read, but still write data.
97        rotate.combineActive(reader, writer("foo"), currentTime);
98        reader.assertRead();
99        assertReadAll(rotate, "foo");
100
101        // second combine should replace contents; should read existing data,
102        // and write final data to disk.
103        currentTime += SECOND_IN_MILLIS;
104        reader.reset();
105        rotate.combineActive(reader, writer("bar"), currentTime);
106        reader.assertRead("foo");
107        assertReadAll(rotate, "bar");
108    }
109
110    public void testRotate() throws Exception {
111        final FileRotator rotate = new FileRotator(
112                mBasePath, PREFIX, DAY_IN_MILLIS, WEEK_IN_MILLIS);
113
114        final RecordingReader reader = new RecordingReader();
115        long currentTime = TEST_TIME;
116
117        // combine first record into file
118        rotate.combineActive(reader, writer("foo"), currentTime);
119        reader.assertRead();
120        assertReadAll(rotate, "foo");
121
122        // push time a few minutes forward; shouldn't rotate file
123        reader.reset();
124        currentTime += MINUTE_IN_MILLIS;
125        rotate.combineActive(reader, writer("bar"), currentTime);
126        reader.assertRead("foo");
127        assertReadAll(rotate, "bar");
128
129        // push time forward enough to rotate file; should still have same data
130        currentTime += DAY_IN_MILLIS + SECOND_IN_MILLIS;
131        rotate.maybeRotate(currentTime);
132        assertReadAll(rotate, "bar");
133
134        // combine a second time, should leave rotated value untouched, and
135        // active file should be empty.
136        reader.reset();
137        rotate.combineActive(reader, writer("baz"), currentTime);
138        reader.assertRead();
139        assertReadAll(rotate, "bar", "baz");
140    }
141
142    public void testDelete() throws Exception {
143        final FileRotator rotate = new FileRotator(
144                mBasePath, PREFIX, MINUTE_IN_MILLIS, DAY_IN_MILLIS);
145
146        final RecordingReader reader = new RecordingReader();
147        long currentTime = TEST_TIME;
148
149        // create first record and trigger rotating it
150        rotate.combineActive(reader, writer("foo"), currentTime);
151        reader.assertRead();
152        currentTime += MINUTE_IN_MILLIS + SECOND_IN_MILLIS;
153        rotate.maybeRotate(currentTime);
154
155        // create second record
156        reader.reset();
157        rotate.combineActive(reader, writer("bar"), currentTime);
158        reader.assertRead();
159        assertReadAll(rotate, "foo", "bar");
160
161        // push time far enough to expire first record
162        currentTime = TEST_TIME + DAY_IN_MILLIS + (2 * MINUTE_IN_MILLIS);
163        rotate.maybeRotate(currentTime);
164        assertReadAll(rotate, "bar");
165
166        // push further to delete second record
167        currentTime += WEEK_IN_MILLIS;
168        rotate.maybeRotate(currentTime);
169        assertReadAll(rotate);
170    }
171
172    public void testThrowRestoresBackup() throws Exception {
173        final FileRotator rotate = new FileRotator(
174                mBasePath, PREFIX, MINUTE_IN_MILLIS, DAY_IN_MILLIS);
175
176        final RecordingReader reader = new RecordingReader();
177        long currentTime = TEST_TIME;
178
179        // first, write some valid data
180        rotate.combineActive(reader, writer("foo"), currentTime);
181        reader.assertRead();
182        assertReadAll(rotate, "foo");
183
184        try {
185            // now, try writing which will throw
186            reader.reset();
187            rotate.combineActive(reader, new Writer() {
188                public void write(OutputStream out) throws IOException {
189                    new DataOutputStream(out).writeUTF("bar");
190                    throw new NullPointerException("yikes");
191                }
192            }, currentTime);
193
194            fail("woah, somehow able to write exception");
195        } catch (IOException e) {
196            // expected from above
197        }
198
199        // assert that we read original data, and that it's still intact after
200        // the failed write above.
201        reader.assertRead("foo");
202        assertReadAll(rotate, "foo");
203    }
204
205    public void testOtherFilesAndMalformed() throws Exception {
206        final FileRotator rotate = new FileRotator(
207                mBasePath, PREFIX, SECOND_IN_MILLIS, SECOND_IN_MILLIS);
208
209        // should ignore another prefix
210        touch("another_rotator.1024");
211        touch("another_rotator.1024-2048");
212        assertReadAll(rotate);
213
214        // verify that broken filenames don't crash
215        touch("rotator");
216        touch("rotator...");
217        touch("rotator.-");
218        touch("rotator.---");
219        touch("rotator.a-b");
220        touch("rotator_but_not_actually");
221        assertReadAll(rotate);
222
223        // and make sure that we can read something from a legit file
224        write("rotator.100-200", "meow");
225        assertReadAll(rotate, "meow");
226    }
227
228    private static final String RED = "red";
229    private static final String GREEN = "green";
230    private static final String BLUE = "blue";
231    private static final String YELLOW = "yellow";
232
233    public void testQueryMatch() throws Exception {
234        final FileRotator rotate = new FileRotator(
235                mBasePath, PREFIX, HOUR_IN_MILLIS, YEAR_IN_MILLIS);
236
237        final RecordingReader reader = new RecordingReader();
238        long currentTime = TEST_TIME;
239
240        // rotate a bunch of historical data
241        rotate.maybeRotate(currentTime);
242        rotate.combineActive(reader, writer(RED), currentTime);
243
244        currentTime += DAY_IN_MILLIS;
245        rotate.maybeRotate(currentTime);
246        rotate.combineActive(reader, writer(GREEN), currentTime);
247
248        currentTime += DAY_IN_MILLIS;
249        rotate.maybeRotate(currentTime);
250        rotate.combineActive(reader, writer(BLUE), currentTime);
251
252        currentTime += DAY_IN_MILLIS;
253        rotate.maybeRotate(currentTime);
254        rotate.combineActive(reader, writer(YELLOW), currentTime);
255
256        final String[] FULL_SET = { RED, GREEN, BLUE, YELLOW };
257
258        assertReadAll(rotate, FULL_SET);
259        assertReadMatching(rotate, Long.MIN_VALUE, Long.MAX_VALUE, FULL_SET);
260        assertReadMatching(rotate, Long.MIN_VALUE, currentTime, FULL_SET);
261        assertReadMatching(rotate, TEST_TIME + SECOND_IN_MILLIS, currentTime, FULL_SET);
262
263        // should omit last value, since it only touches at currentTime
264        assertReadMatching(rotate, TEST_TIME + SECOND_IN_MILLIS, currentTime - SECOND_IN_MILLIS,
265                RED, GREEN, BLUE);
266
267        // check boundary condition
268        assertReadMatching(rotate, TEST_TIME + DAY_IN_MILLIS, Long.MAX_VALUE, FULL_SET);
269        assertReadMatching(rotate, TEST_TIME + DAY_IN_MILLIS + SECOND_IN_MILLIS, Long.MAX_VALUE,
270                GREEN, BLUE, YELLOW);
271
272        // test range smaller than file
273        final long blueStart = TEST_TIME + (DAY_IN_MILLIS * 2);
274        final long blueEnd = TEST_TIME + (DAY_IN_MILLIS * 3);
275        assertReadMatching(rotate, blueStart + SECOND_IN_MILLIS, blueEnd - SECOND_IN_MILLIS, BLUE);
276
277        // outside range should return nothing
278        assertReadMatching(rotate, Long.MIN_VALUE, TEST_TIME - DAY_IN_MILLIS);
279    }
280
281    public void testClockRollingBackwards() throws Exception {
282        final FileRotator rotate = new FileRotator(
283                mBasePath, PREFIX, DAY_IN_MILLIS, YEAR_IN_MILLIS);
284
285        final RecordingReader reader = new RecordingReader();
286        long currentTime = TEST_TIME;
287
288        // create record at current time
289        // --> foo
290        rotate.combineActive(reader, writer("foo"), currentTime);
291        reader.assertRead();
292        assertReadAll(rotate, "foo");
293
294        // record a day in past; should create a new active file
295        // --> bar
296        currentTime -= DAY_IN_MILLIS;
297        reader.reset();
298        rotate.combineActive(reader, writer("bar"), currentTime);
299        reader.assertRead();
300        assertReadAll(rotate, "bar", "foo");
301
302        // verify that we rewrite current active file
303        // bar --> baz
304        currentTime += SECOND_IN_MILLIS;
305        reader.reset();
306        rotate.combineActive(reader, writer("baz"), currentTime);
307        reader.assertRead("bar");
308        assertReadAll(rotate, "baz", "foo");
309
310        // return to present and verify we write oldest active file
311        // baz --> meow
312        currentTime = TEST_TIME + SECOND_IN_MILLIS;
313        reader.reset();
314        rotate.combineActive(reader, writer("meow"), currentTime);
315        reader.assertRead("baz");
316        assertReadAll(rotate, "meow", "foo");
317
318        // current time should trigger rotate of older active file
319        rotate.maybeRotate(currentTime);
320
321        // write active file, verify this time we touch original
322        // foo --> yay
323        reader.reset();
324        rotate.combineActive(reader, writer("yay"), currentTime);
325        reader.assertRead("foo");
326        assertReadAll(rotate, "meow", "yay");
327    }
328
329    @Suppress
330    public void testFuzz() throws Exception {
331        final FileRotator rotate = new FileRotator(
332                mBasePath, PREFIX, HOUR_IN_MILLIS, DAY_IN_MILLIS);
333
334        final RecordingReader reader = new RecordingReader();
335        long currentTime = TEST_TIME;
336
337        // walk forward through time, ensuring that files are cleaned properly
338        final Random random = new Random();
339        for (int i = 0; i < 1024; i++) {
340            currentTime += Math.abs(random.nextLong()) % DAY_IN_MILLIS;
341
342            reader.reset();
343            rotate.combineActive(reader, writer("meow"), currentTime);
344
345            if (random.nextBoolean()) {
346                rotate.maybeRotate(currentTime);
347            }
348        }
349
350        rotate.maybeRotate(currentTime);
351
352        Log.d(TAG, "currentTime=" + currentTime);
353        Log.d(TAG, Arrays.toString(mBasePath.list()));
354    }
355
356    public void testRecoverAtomic() throws Exception {
357        write("rotator.1024-2048", "foo");
358        write("rotator.1024-2048.backup", "bar");
359        write("rotator.2048-4096", "baz");
360        write("rotator.2048-4096.no_backup", "");
361
362        final FileRotator rotate = new FileRotator(
363                mBasePath, PREFIX, SECOND_IN_MILLIS, SECOND_IN_MILLIS);
364
365        // verify backup value was recovered; no_backup indicates that
366        // corresponding file had no backup and should be discarded.
367        assertReadAll(rotate, "bar");
368    }
369
370    private void touch(String... names) throws IOException {
371        for (String name : names) {
372            final OutputStream out = new FileOutputStream(new File(mBasePath, name));
373            out.close();
374        }
375    }
376
377    private void write(String name, String value) throws IOException {
378        final DataOutputStream out = new DataOutputStream(
379                new FileOutputStream(new File(mBasePath, name)));
380        out.writeUTF(value);
381        out.close();
382    }
383
384    private static Writer writer(final String value) {
385        return new Writer() {
386            public void write(OutputStream out) throws IOException {
387                new DataOutputStream(out).writeUTF(value);
388            }
389        };
390    }
391
392    private static void assertReadAll(FileRotator rotate, String... expected) throws IOException {
393        assertReadMatching(rotate, Long.MIN_VALUE, Long.MAX_VALUE, expected);
394    }
395
396    private static void assertReadMatching(
397            FileRotator rotate, long matchStartMillis, long matchEndMillis, String... expected)
398            throws IOException {
399        final RecordingReader reader = new RecordingReader();
400        rotate.readMatching(reader, matchStartMillis, matchEndMillis);
401        reader.assertRead(expected);
402    }
403
404    private static class RecordingReader implements Reader {
405        private ArrayList<String> mActual = Lists.newArrayList();
406
407        public void read(InputStream in) throws IOException {
408            mActual.add(new DataInputStream(in).readUTF());
409        }
410
411        public void reset() {
412            mActual.clear();
413        }
414
415        public void assertRead(String... expected) {
416            assertEquals(expected.length, mActual.size());
417
418            final ArrayList<String> actualCopy = new ArrayList<String>(mActual);
419            for (String value : expected) {
420                if (!actualCopy.remove(value)) {
421                    final String expectedString = Arrays.toString(expected);
422                    final String actualString = Arrays.toString(mActual.toArray());
423                    fail("expected: " + expectedString + " but was: " + actualString);
424                }
425            }
426        }
427    }
428}
429