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