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