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