1 /* 2 * Copyright (C) 2020 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 android.util; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.junit.Assert.assertArrayEquals; 22 import static org.junit.Assert.assertTrue; 23 24 import android.app.Instrumentation; 25 import android.content.Context; 26 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 import androidx.test.platform.app.InstrumentationRegistry; 30 31 import org.junit.After; 32 import org.junit.Before; 33 import org.junit.Test; 34 import org.junit.runner.RunWith; 35 import org.junit.runners.Parameterized; 36 37 import java.io.ByteArrayOutputStream; 38 import java.io.File; 39 import java.io.FileInputStream; 40 import java.io.FileNotFoundException; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.nio.charset.StandardCharsets; 45 46 @RunWith(Parameterized.class) 47 public class AtomicFileTest { 48 private static final String BASE_NAME = "base"; 49 private static final String NEW_NAME = BASE_NAME + ".new"; 50 private static final String LEGACY_BACKUP_NAME = BASE_NAME + ".bak"; 51 // The string isn't actually used, but we just need a different identifier. 52 private static final String BASE_NAME_DIRECTORY = BASE_NAME + ".dir"; 53 54 private enum WriteAction { 55 FINISH, 56 FAIL, 57 ABORT, 58 READ_FINISH 59 } 60 61 private static final byte[] BASE_BYTES = "base".getBytes(StandardCharsets.UTF_8); 62 private static final byte[] EXISTING_NEW_BYTES = "unnew".getBytes(StandardCharsets.UTF_8); 63 private static final byte[] NEW_BYTES = "new".getBytes(StandardCharsets.UTF_8); 64 private static final byte[] LEGACY_BACKUP_BYTES = "bak".getBytes(StandardCharsets.UTF_8); 65 66 // JUnit wants every parameter to be used so make it happy. 67 @Parameterized.Parameter() 68 public String mUnusedTestName; 69 @Nullable 70 @Parameterized.Parameter(1) 71 public String[] mExistingFileNames; 72 @Nullable 73 @Parameterized.Parameter(2) 74 public WriteAction mWriteAction; 75 @Nullable 76 @Parameterized.Parameter(3) 77 public byte[] mExpectedBytes; 78 79 private final Instrumentation mInstrumentation = 80 InstrumentationRegistry.getInstrumentation(); 81 private final Context mContext = mInstrumentation.getContext(); 82 83 private final File mDirectory = mContext.getFilesDir(); 84 private final File mBaseFile = new File(mDirectory, BASE_NAME); 85 private final File mNewFile = new File(mDirectory, NEW_NAME); 86 private final File mLegacyBackupFile = new File(mDirectory, LEGACY_BACKUP_NAME); 87 88 @Parameterized.Parameters(name = "{0}") data()89 public static Object[][] data() { 90 return new Object[][] { 91 // Standard tests. 92 { "none + none = none", null, null, null }, 93 { "none + finish = new", null, WriteAction.FINISH, NEW_BYTES }, 94 { "none + fail = none", null, WriteAction.FAIL, null }, 95 { "none + abort = none", null, WriteAction.ABORT, null }, 96 { "base + none = base", new String[] { BASE_NAME }, null, BASE_BYTES }, 97 { "base + finish = new", new String[] { BASE_NAME }, WriteAction.FINISH, 98 NEW_BYTES }, 99 { "base + fail = base", new String[] { BASE_NAME }, WriteAction.FAIL, BASE_BYTES }, 100 { "base + abort = base", new String[] { BASE_NAME }, WriteAction.ABORT, 101 BASE_BYTES }, 102 { "new + none = none", new String[] { NEW_NAME }, null, null }, 103 { "new + finish = new", new String[] { NEW_NAME }, WriteAction.FINISH, NEW_BYTES }, 104 { "new + fail = none", new String[] { NEW_NAME }, WriteAction.FAIL, null }, 105 { "new + abort = none", new String[] { NEW_NAME }, WriteAction.ABORT, null }, 106 { "bak + none = bak", new String[] { LEGACY_BACKUP_NAME }, null, 107 LEGACY_BACKUP_BYTES }, 108 { "bak + finish = new", new String[] { LEGACY_BACKUP_NAME }, WriteAction.FINISH, 109 NEW_BYTES }, 110 { "bak + fail = bak", new String[] { LEGACY_BACKUP_NAME }, WriteAction.FAIL, 111 LEGACY_BACKUP_BYTES }, 112 { "bak + abort = bak", new String[] { LEGACY_BACKUP_NAME }, WriteAction.ABORT, 113 LEGACY_BACKUP_BYTES }, 114 { "base & new + none = base", new String[] { BASE_NAME, NEW_NAME }, null, 115 BASE_BYTES }, 116 { "base & new + finish = new", new String[] { BASE_NAME, NEW_NAME }, 117 WriteAction.FINISH, NEW_BYTES }, 118 { "base & new + fail = base", new String[] { BASE_NAME, NEW_NAME }, 119 WriteAction.FAIL, BASE_BYTES }, 120 { "base & new + abort = base", new String[] { BASE_NAME, NEW_NAME }, 121 WriteAction.ABORT, BASE_BYTES }, 122 { "base & bak + none = bak", new String[] { BASE_NAME, LEGACY_BACKUP_NAME }, null, 123 LEGACY_BACKUP_BYTES }, 124 { "base & bak + finish = new", new String[] { BASE_NAME, LEGACY_BACKUP_NAME }, 125 WriteAction.FINISH, NEW_BYTES }, 126 { "base & bak + fail = bak", new String[] { BASE_NAME, LEGACY_BACKUP_NAME }, 127 WriteAction.FAIL, LEGACY_BACKUP_BYTES }, 128 { "base & bak + abort = bak", new String[] { BASE_NAME, LEGACY_BACKUP_NAME }, 129 WriteAction.ABORT, LEGACY_BACKUP_BYTES }, 130 { "new & bak + none = bak", new String[] { NEW_NAME, LEGACY_BACKUP_NAME }, null, 131 LEGACY_BACKUP_BYTES }, 132 { "new & bak + finish = new", new String[] { NEW_NAME, LEGACY_BACKUP_NAME }, 133 WriteAction.FINISH, NEW_BYTES }, 134 { "new & bak + fail = bak", new String[] { NEW_NAME, LEGACY_BACKUP_NAME }, 135 WriteAction.FAIL, LEGACY_BACKUP_BYTES }, 136 { "new & bak + abort = bak", new String[] { NEW_NAME, LEGACY_BACKUP_NAME }, 137 WriteAction.ABORT, LEGACY_BACKUP_BYTES }, 138 { "base & new & bak + none = bak", 139 new String[] { BASE_NAME, NEW_NAME, LEGACY_BACKUP_NAME }, null, 140 LEGACY_BACKUP_BYTES }, 141 { "base & new & bak + finish = new", 142 new String[] { BASE_NAME, NEW_NAME, LEGACY_BACKUP_NAME }, 143 WriteAction.FINISH, NEW_BYTES }, 144 { "base & new & bak + fail = bak", 145 new String[] { BASE_NAME, NEW_NAME, LEGACY_BACKUP_NAME }, WriteAction.FAIL, 146 LEGACY_BACKUP_BYTES }, 147 { "base & new & bak + abort = bak", 148 new String[] { BASE_NAME, NEW_NAME, LEGACY_BACKUP_NAME }, WriteAction.ABORT, 149 LEGACY_BACKUP_BYTES }, 150 // Compatibility when there is a directory in the place of base file, by replacing 151 // no base with base.dir. 152 { "base.dir + none = none", new String[] { BASE_NAME_DIRECTORY }, null, null }, 153 { "base.dir + finish = new", new String[] { BASE_NAME_DIRECTORY }, 154 WriteAction.FINISH, NEW_BYTES }, 155 { "base.dir + fail = none", new String[] { BASE_NAME_DIRECTORY }, WriteAction.FAIL, 156 null }, 157 { "base.dir + abort = none", new String[] { BASE_NAME_DIRECTORY }, 158 WriteAction.ABORT, null }, 159 { "base.dir & new + none = none", new String[] { BASE_NAME_DIRECTORY, NEW_NAME }, 160 null, null }, 161 { "base.dir & new + finish = new", new String[] { BASE_NAME_DIRECTORY, NEW_NAME }, 162 WriteAction.FINISH, NEW_BYTES }, 163 { "base.dir & new + fail = none", new String[] { BASE_NAME_DIRECTORY, NEW_NAME }, 164 WriteAction.FAIL, null }, 165 { "base.dir & new + abort = none", new String[] { BASE_NAME_DIRECTORY, NEW_NAME }, 166 WriteAction.ABORT, null }, 167 { "base.dir & bak + none = bak", 168 new String[] { BASE_NAME_DIRECTORY, LEGACY_BACKUP_NAME }, null, 169 LEGACY_BACKUP_BYTES }, 170 { "base.dir & bak + finish = new", 171 new String[] { BASE_NAME_DIRECTORY, LEGACY_BACKUP_NAME }, 172 WriteAction.FINISH, NEW_BYTES }, 173 { "base.dir & bak + fail = bak", 174 new String[] { BASE_NAME_DIRECTORY, LEGACY_BACKUP_NAME }, WriteAction.FAIL, 175 LEGACY_BACKUP_BYTES }, 176 { "base.dir & bak + abort = bak", 177 new String[] { BASE_NAME_DIRECTORY, LEGACY_BACKUP_NAME }, WriteAction.ABORT, 178 LEGACY_BACKUP_BYTES }, 179 { "base.dir & new & bak + none = bak", 180 new String[] { BASE_NAME_DIRECTORY, NEW_NAME, LEGACY_BACKUP_NAME }, null, 181 LEGACY_BACKUP_BYTES }, 182 { "base.dir & new & bak + finish = new", 183 new String[] { BASE_NAME_DIRECTORY, NEW_NAME, LEGACY_BACKUP_NAME }, 184 WriteAction.FINISH, NEW_BYTES }, 185 { "base.dir & new & bak + fail = bak", 186 new String[] { BASE_NAME_DIRECTORY, NEW_NAME, LEGACY_BACKUP_NAME }, 187 WriteAction.FAIL, LEGACY_BACKUP_BYTES }, 188 { "base.dir & new & bak + abort = bak", 189 new String[] { BASE_NAME_DIRECTORY, NEW_NAME, LEGACY_BACKUP_NAME }, 190 WriteAction.ABORT, LEGACY_BACKUP_BYTES }, 191 // Compatibility when openRead() is called between startWrite() and finishWrite() - 192 // the write should still succeed if it's the first write. 193 { "none + read & finish = new", null, WriteAction.READ_FINISH, NEW_BYTES }, 194 }; 195 } 196 197 @Before 198 @After deleteFiles()199 public void deleteFiles() { 200 mBaseFile.delete(); 201 mNewFile.delete(); 202 mLegacyBackupFile.delete(); 203 } 204 205 @Test testAtomicFile()206 public void testAtomicFile() throws Exception { 207 if (mExistingFileNames != null) { 208 for (String fileName : mExistingFileNames) { 209 switch (fileName) { 210 case BASE_NAME: 211 writeBytes(mBaseFile, BASE_BYTES); 212 break; 213 case NEW_NAME: 214 writeBytes(mNewFile, EXISTING_NEW_BYTES); 215 break; 216 case LEGACY_BACKUP_NAME: 217 writeBytes(mLegacyBackupFile, LEGACY_BACKUP_BYTES); 218 break; 219 case BASE_NAME_DIRECTORY: 220 assertTrue(mBaseFile.mkdir()); 221 break; 222 default: 223 throw new AssertionError(fileName); 224 } 225 } 226 } 227 228 AtomicFile atomicFile = new AtomicFile(mBaseFile); 229 if (mWriteAction != null) { 230 try (FileOutputStream outputStream = atomicFile.startWrite()) { 231 outputStream.write(NEW_BYTES); 232 switch (mWriteAction) { 233 case FINISH: 234 atomicFile.finishWrite(outputStream); 235 break; 236 case FAIL: 237 atomicFile.failWrite(outputStream); 238 break; 239 case ABORT: 240 // Neither finishing nor failing is called upon abort. 241 break; 242 case READ_FINISH: 243 // We are only using this action when there is no base file. 244 assertThrows(FileNotFoundException.class, atomicFile::openRead); 245 atomicFile.finishWrite(outputStream); 246 break; 247 default: 248 throw new AssertionError(mWriteAction); 249 } 250 } 251 } 252 253 if (mExpectedBytes != null) { 254 try (FileInputStream inputStream = atomicFile.openRead()) { 255 assertArrayEquals(mExpectedBytes, readAllBytes(inputStream)); 256 } 257 } else { 258 assertThrows(FileNotFoundException.class, atomicFile::openRead); 259 } 260 } 261 262 @Test testToString()263 public void testToString() throws Exception { 264 AtomicFile atomicFile = new AtomicFile(mBaseFile); 265 266 String toString = atomicFile.toString(); 267 268 assertThat(toString).contains("AtomicFile"); 269 assertThat(toString).contains(mBaseFile.getAbsolutePath()); 270 } 271 writeBytes(@onNull File file, @NonNull byte[] bytes)272 private static void writeBytes(@NonNull File file, @NonNull byte[] bytes) throws IOException { 273 try (FileOutputStream outputStream = new FileOutputStream(file)) { 274 outputStream.write(bytes); 275 } 276 } 277 278 // InputStream.readAllBytes() is introduced in Java 9. Our files are small enough so that a 279 // naive implementation is okay. readAllBytes(@onNull InputStream inputStream)280 private static byte[] readAllBytes(@NonNull InputStream inputStream) throws IOException { 281 try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { 282 int b; 283 while ((b = inputStream.read()) != -1) { 284 outputStream.write(b); 285 } 286 return outputStream.toByteArray(); 287 } 288 } 289 290 @NonNull assertThrows(@onNull Class<T> expectedType, @NonNull ThrowingRunnable runnable)291 public static <T extends Throwable> T assertThrows(@NonNull Class<T> expectedType, 292 @NonNull ThrowingRunnable runnable) { 293 try { 294 runnable.run(); 295 } catch (Throwable t) { 296 if (!expectedType.isInstance(t)) { 297 sneakyThrow(t); 298 } 299 //noinspection unchecked 300 return (T) t; 301 } 302 throw new AssertionError(String.format("Expected %s wasn't thrown", 303 expectedType.getSimpleName())); 304 } 305 sneakyThrow(@onNull Throwable throwable)306 private static <T extends Throwable> void sneakyThrow(@NonNull Throwable throwable) throws T { 307 //noinspection unchecked 308 throw (T) throwable; 309 } 310 311 private interface ThrowingRunnable { run()312 void run() throws Throwable; 313 } 314 } 315