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