1 /* 2 * Copyright 2022 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.datastore 18 19 import androidx.datastore.core.DataStore 20 import androidx.datastore.core.DataStoreFactory.create 21 import androidx.datastore.core.InterProcessCoordinator 22 import androidx.datastore.core.Storage 23 import kotlin.random.Random 24 import kotlin.reflect.KClass 25 import kotlinx.coroutines.CoroutineScope 26 27 abstract class TestIO<F : TestFile<F>, IOE : Throwable>(getTmpDir: () -> F) { 28 private val testRoot: F = <lambda>null29 getTmpDir().let { createNewRandomChild(parent = it, namePrefix = "datastore-testio-") } 30 31 init { 32 testRoot.mkdirs(mustCreate = true) 33 } 34 getStorenull35 fun getStore( 36 serializerConfig: TestingSerializerConfig, 37 scope: CoroutineScope, 38 coordinatorProducer: () -> InterProcessCoordinator, 39 futureFile: () -> F 40 ): DataStore<Byte> { 41 return create(getStorage(serializerConfig, coordinatorProducer, futureFile), scope = scope) 42 } 43 getStoragenull44 abstract fun getStorage( 45 serializerConfig: TestingSerializerConfig, 46 coordinatorProducer: () -> InterProcessCoordinator, 47 futureFile: () -> F = { newTempFile() } 48 ): Storage<Byte> 49 randomNamenull50 private fun randomName(prefix: String): String { 51 return prefix + 52 (0 until 15).joinToString(separator = "") { 53 ('a' + Random.nextInt(from = 0, until = 26)).toString() 54 } 55 } 56 createNewRandomChildnull57 protected fun createNewRandomChild( 58 parent: F, 59 namePrefix: String = "test-file-", 60 ): F { 61 while (true) { 62 val child = parent.resolve(randomName(namePrefix)) 63 if (!child.exists()) { 64 return child 65 } 66 } 67 } 68 69 /** Returns a new file instance without creating it or its parents. */ newTempFilenull70 fun newTempFile(parentFile: F? = null, relativePath: String? = null): F { 71 val parent = parentFile ?: testRoot 72 return if (relativePath == null) { 73 createNewRandomChild(parent = parent) 74 } else { 75 parent.resolve(relativePath) 76 } 77 } 78 ioExceptionnull79 abstract fun ioException(message: String): IOE 80 81 abstract fun ioExceptionClass(): KClass<IOE> 82 } 83 84 abstract class TestFile<T : TestFile<T>> { 85 /** The name of the file, including the extension */ 86 abstract val name: String 87 88 /** Get the canonical path for the file. */ 89 abstract fun path(): String 90 91 /** 92 * Deletes the file if it exists. Will return `false` if the file does not exist or cannot be 93 * deleted. (similar to File.delete) 94 */ 95 abstract fun delete(): Boolean 96 97 /** Returns true if this file/directory exists. */ 98 abstract fun exists(): Boolean 99 100 /** 101 * Creates a directory from the file. 102 * 103 * If it is a regular file, will throw. If [mustCreate] is `true` and directory already exists, 104 * will throw. 105 */ 106 abstract fun mkdirs(mustCreate: Boolean = false) 107 108 /** Returns `true` if this exists and a regular file on the filesystem. */ 109 abstract fun isRegularFile(): Boolean 110 111 /** Returns `true` if this exists and is a directory on the filesystem. */ 112 abstract fun isDirectory(): Boolean 113 114 /** 115 * Resolves the given [relative] relative to `this`. (similar to File.resolve in kotlin stdlib). 116 * 117 * Note that this path is sanitized to ensure it is not a root path (e.g. does not start with 118 * `/`) 119 */ 120 fun resolve(relative: String): T { 121 return if (relative.startsWith("/")) { 122 protectedResolve(relative.substring(startIndex = 1)) 123 } else { 124 protectedResolve(relative) 125 } 126 } 127 128 /** Implemented by the subclasses to resolve child from sanitized name. */ 129 abstract fun protectedResolve(relative: String): T 130 131 /** Returns the parent file or null if this has no parent. */ 132 abstract fun parentFile(): T? 133 134 /** 135 * Writes the given [body] test into the file using the default encoding (kotlin stdlib's 136 * String.encodeToByteArray) 137 */ 138 fun write(body: String) { 139 write(body.encodeToByteArray()) 140 } 141 142 /** 143 * Overrides the file with the given contents. If parent directories do not exist, they'll be 144 * created. 145 */ 146 fun write(body: ByteArray) { 147 if (isDirectory()) { 148 error("Cannot write to a directory") 149 } 150 parentFile()?.mkdirs(mustCreate = false) 151 parentFile()?.mkdirs() 152 protectedWrite(body) 153 } 154 155 /** Reads the byte contents of the file. */ 156 fun readBytes(): ByteArray { 157 check(exists()) { "File does not exist" } 158 return protectedReadBytes() 159 } 160 161 fun readText() = readBytes().decodeToString() 162 163 /** 164 * Writes the given [body] into the file. This is called after necessary checks are done so 165 * implementers should only focus on writing the contents. 166 */ 167 abstract fun protectedWrite(body: ByteArray) 168 169 /** 170 * Reads the byte contents of the file. This is called after necessary checks are done so 171 * implementers should only focus on reading the bytes. 172 */ 173 abstract fun protectedReadBytes(): ByteArray 174 } 175