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