• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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 com.android.tools.metalava.testing
18 
19 import com.android.tools.lint.checks.infrastructure.TestFile
20 import java.io.File
21 import java.nio.file.Files
22 import org.junit.rules.TemporaryFolder
23 import org.junit.rules.TestRule
24 import org.junit.runner.Description
25 import org.junit.runners.model.Statement
26 
27 /**
28  * Cache [TestFile]s such that they can be easily shared between tests.
29  *
30  * The primary purpose of this is to amortize the cost of creating jars for testing by sharing the
31  * resulting [File] across multiple tests at the cost of increased disk usage. However, it can be
32  * used to cache, and share, any [TestFile] whose [TestFile.createFile] will create a [File] in the
33  * supplied `targetDir`.
34  *
35  * Must call [open] before calling [TestFile.createFile] on any [TestFile] returned from [cache].
36  */
37 class TestFileCache : AutoCloseable {
38     /** The directory in which [File]s are created. */
39     private lateinit var cacheDir: File
40 
41     /** Map from [TestFile.targetRelativePath] to [CachedTestFile] wrapper. */
42     private val targetRelativePathToTestFile = mutableMapOf<String, CachedTestFile>()
43 
44     /**
45      * Open the cache for use by supplying it with a [cacheDir] in which it will create the [File]s.
46      *
47      * This can be called after calling [cache] to cache a [TestFile] but must be called before any
48      * of those [TestFile]s are created. Separating this from initialization allows the cache to be
49      * created and used to cache [TestFile]s without creating a directory that may never be used and
50      * may not be easily cleaned up.
51      *
52      * It is the responsibility of the caller of [open] to ensure that [cacheDir] is deleted, either
53      * by calling [close] or some other way, e.g. by passing in a directory created by
54      * [TemporaryFolder].
55      */
opennull56     fun open(cacheDir: File): TestFileCache {
57         if (::cacheDir.isInitialized) {
58             error(
59                 "Cannot open cache with `$cacheDir` as it has already been opened with `${this.cacheDir}"
60             )
61         }
62         this.cacheDir = cacheDir
63         return this
64     }
65 
66     /**
67      * Cache a [TestFile] for sharing.
68      *
69      * This returns a [TestFile] wrapper around [testFile] that will ensure that [testFile] will
70      * only be created once within this cache. The [TestFile.targetRelativePath] must uniquely
71      * identify [testFile] among all [TestFile]s added to this cache.
72      */
cachenull73     fun cache(testFile: TestFile): TestFile {
74         val relative =
75             testFile.targetRelativePath
76                 ?: error("Cannot cache $testFile as it has no targetRelativePath")
77         val existing = targetRelativePathToTestFile[relative]
78         if (existing != null) {
79             if (existing.underlying !== testFile) {
80                 error(
81                     "Cannot cache $testFile as its targetRelativePath of `$relative` clashes with $existing"
82                 )
83             }
84             return existing
85         }
86 
87         val cached = CachedTestFile(testFile).apply { to(testFile.targetRelativePath) }
88         targetRelativePathToTestFile[relative] = cached
89         return cached
90     }
91 
closenull92     override fun close() {
93         if (::cacheDir.isInitialized) {
94             cacheDir.deleteRecursively()
95         }
96     }
97 
createUnderlyingFilenull98     private fun createUnderlyingFile(underlyingFile: TestFile): TestFile {
99         if (!::cacheDir.isInitialized) {
100             error("Cannot create underlying file as cache has not yet been opened")
101         }
102         val file = underlyingFile.createFile(cacheDir)
103         // Compute the path of the newly created file relative to the directory.
104         val relative = file.relativeTo(cacheDir).path
105 
106         // Create a TestFile that when created will create a symbolic link to `file` in the same
107         // directory relative to the target directory.
108         return file.toTestFile().to(relative)
109     }
110 
111     /** Wrapper around a [TestFile] that ensures that only one instance will be created. */
112     private inner class CachedTestFile(val underlying: TestFile) : TestFile() {
113         /** Lazily create the [File] from [underlying] and wrap in a [TestFile]. */
<lambda>null114         private val file by lazy { createUnderlyingFile(underlying) }
115 
createFilenull116         override fun createFile(targetDir: File?): File =
117             // Create the underlying file if necessary and then create a symbolic link to it.
118             file.createFile(targetDir)
119     }
120 }
121 
122 /**
123  * A [TestRule] that is intended to be used with `@ClassRule` to create a per test class cache of
124  * [TestFile]s.
125  *
126  * It can also be used with `@Rule` to create a per test cache if a [TestFile] might be created
127  * multiple times within the same test.
128  */
129 class TestFileCacheRule : TestRule {
130     /**
131      * Create the [TestFileCache] immediately so it can be used to cache [TestFile]s before entering
132      * the [Statement] returned by [apply].
133      */
134     private var _cache: TestFileCache? = TestFileCache()
135 
136     /** Get the cache to use. */
137     val cache: TestFileCache
138         get() = _cache ?: error("Cannot access cache outside test run")
139 
140     override fun apply(base: Statement, description: Description): Statement {
141         return object : Statement() {
142             override fun evaluate() {
143                 try {
144                     val cacheDir = Files.createTempDirectory("test-file-cache").toFile()
145                     _cache!!.open(cacheDir).use { base.evaluate() }
146                 } finally {
147                     // Prevent the cache from being accessed after it has been used.
148                     _cache = null
149                 }
150             }
151         }
152     }
153 }
154 
155 /**
156  * Cache this [TestFile] in [cache].
157  *
158  * Returns a [TestFile] that will only create this file once and will use a symbolic link to add it
159  * to the `targetDir` of [TestFile.createFile].
160  */
cacheInnull161 fun TestFile.cacheIn(cache: TestFileCache) = cache.cache(this)
162 
163 /**
164  * Cache this [TestFile] in [cacheRule]'s [TestFileCacheRule.cache].
165  *
166  * Returns a [TestFile] that will only create this file once and will use a symbolic link to add it
167  * to the `targetDir` of [TestFile.createFile].
168  */
169 fun TestFile.cacheIn(cacheRule: TestFileCacheRule) = cacheRule.cache.cache(this)
170