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