Home | History | Annotate | Download | only in util
      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 
     17 package com.android.internal.util;
     18 
     19 import static android.text.format.DateUtils.DAY_IN_MILLIS;
     20 import static android.text.format.DateUtils.HOUR_IN_MILLIS;
     21 import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
     22 import static android.text.format.DateUtils.SECOND_IN_MILLIS;
     23 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
     24 import static android.text.format.DateUtils.YEAR_IN_MILLIS;
     25 
     26 import android.test.AndroidTestCase;
     27 import android.test.suitebuilder.annotation.Suppress;
     28 import android.util.Log;
     29 
     30 import com.android.internal.util.FileRotator.Reader;
     31 import com.android.internal.util.FileRotator.Writer;
     32 import com.google.android.collect.Lists;
     33 
     34 import java.io.DataInputStream;
     35 import java.io.DataOutputStream;
     36 import java.io.File;
     37 import java.io.FileOutputStream;
     38 import java.io.IOException;
     39 import java.io.InputStream;
     40 import java.io.OutputStream;
     41 import java.net.ProtocolException;
     42 import java.util.ArrayList;
     43 import java.util.Arrays;
     44 import java.util.Random;
     45 
     46 import libcore.io.IoUtils;
     47 
     48 /**
     49  * Tests for {@link FileRotator}.
     50  */
     51 public 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