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 junit.framework.Assert;
     47 
     48 import libcore.io.IoUtils;
     49 
     50 /**
     51  * Tests for {@link FileRotator}.
     52  */
     53 public 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