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