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.build.importMaven
18 
19 import com.google.common.truth.Truth.assertThat
20 import com.google.common.truth.Truth.assertWithMessage
21 import okio.FileSystem
22 import okio.Path
23 import okio.Path.Companion.toPath
24 import okio.buffer
25 import okio.fakefilesystem.FakeFileSystem
26 import org.junit.Test
27 
28 /**
29  * Integration tests for [ArtifactResolver]
30  */
31 class ArtifactResolverTest {
<lambda>null32     private val fakeFileSystem = FakeFileSystem().also {
33         it.emulateUnix()
34     }
35     private val downloader = LocalMavenRepoDownloader(
36         fileSystem = fakeFileSystem,
37         internalFolder = fakeFileSystem.workingDirectory / "internal",
38         externalFolder = fakeFileSystem.workingDirectory / "external"
39     )
40 
41     @Test
downloadAndroidXPrebuiltnull42     fun downloadAndroidXPrebuilt() {
43         ArtifactResolver.resolveArtifacts(
44             artifacts = listOf("androidx.room:room-runtime:2.5.0-SNAPSHOT"),
45             additionalRepositories = listOf(
46                 ArtifactResolver.createAndroidXRepo(8657806)
47             ),
48             downloadObserver = downloader
49         )
50         assertThat(
51             fakeFileSystem.allPathStrings()
52         ).containsAtLeast(
53             "/internal/androidx/room/room-runtime/2.5.0-SNAPSHOT/maven-metadata.xml",
54             "/internal/androidx/room/room-common/2.5.0-SNAPSHOT/maven-metadata.xml",
55         )
56         // the downloaded file for snapshot will have a version.
57         // If we assert exact name, it will fail when build is no longer available. Instead, we look
58         // into files.
59         val roomRuntimeFiles = fakeFileSystem.list(
60             "/internal/androidx/room/room-runtime/2.5.0-SNAPSHOT/".toPath()
61         )
62         assertWithMessage(
63             roomRuntimeFiles.joinToString("\n") { it.toString() }
64         ).that(
65             roomRuntimeFiles.any {
66                 it.name.startsWith("room-runtime-") &&
67                         it.name.endsWith("aar")
68             }).isTrue()
69     }
70 
71     @Test
testAndroidArtifactsWithMetadatanull72     fun testAndroidArtifactsWithMetadata() {
73         ArtifactResolver.resolveArtifacts(
74             listOf("androidx.room:room-runtime:2.4.2"),
75             downloadObserver = downloader
76         )
77         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
78             "/internal/androidx/room/room-runtime/2.4.2/".toPath().expectedAar(
79                 signed = false,
80                 "room-runtime-2.4.2"
81             ) + "/internal/androidx/room/room-common/2.4.2/".toPath().expectedJar(
82                 signed = false,
83                 "room-common-2.4.2"
84             ) + "/internal/androidx/sqlite/sqlite-framework/2.2.0".toPath().expectedAar(
85                 signed = false,
86                 "sqlite-framework-2.2.0"
87             ) + "/internal/androidx/annotation/annotation/1.1.0".toPath().expectedFiles(
88                 signed = false,
89                 // this annotations artifact is old, it doesn't have module metadata
90                 "annotation-1.1.0.jar", "annotation-1.1.0.pom"
91             )
92         )
93         // don't copy licenses for internal artifacts
94         assertThat(fakeFileSystem.allPathStrings()).doesNotContain(
95             "/internal/androidx/room/room-runtime/2.4.2/LICENSE"
96         )
97     }
98 
99     @Test
testGmavenJarnull100     fun testGmavenJar() {
101         ArtifactResolver.resolveArtifacts(
102             listOf("androidx.room:room-common:2.4.2"),
103             downloadObserver = downloader
104         )
105         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
106             "/internal/androidx/room/room-common/2.4.2/".toPath().expectedJar(
107                 signed = false,
108                 "room-common-2.4.2"
109             ) + "/internal/androidx/annotation/annotation/1.1.0".toPath().expectedFiles(
110                 signed = false,
111                 // this annotations artifact is old, it doesn't have module metadata
112                 "annotation-1.1.0.jar", "annotation-1.1.0.pom"
113             )
114         )
115         // don't copy licenses for internal artifacts
116         assertThat(fakeFileSystem.allPathStrings()).doesNotContain(
117             "/internal/androidx/room/room-runtime/2.4.2/LICENSE"
118         )
119     }
120 
121     @Test
ensureInternalPomsAreReWrittennull122     fun ensureInternalPomsAreReWritten() {
123         val bytes = this::class.java
124             .getResourceAsStream("/pom-with-aar-deps.pom")!!.readBytes()
125         downloader.onDownload("notAndroidx/subject.pom", bytes)
126         val externalContents = fakeFileSystem.read(
127             "external/notAndroidx/subject.pom".toPath()
128         ) {
129             this.readUtf8()
130         }.lines().map { it.trim() }
131         assertThat(externalContents).contains("<type>aar</type>")
132         assertThat(externalContents).doesNotContain("<!--<type>aar</type>-->")
133 
134         downloader.onDownload("androidx/subject.pom", bytes)
135         val internalContents = fakeFileSystem.read(
136             "internal/androidx/subject.pom".toPath()
137         ) {
138             this.readUtf8()
139         }.lines().map { it.trim() }
140         assertThat(internalContents).doesNotContain("<type>aar</type>")
141         assertThat(internalContents).contains("<!--<type>aar</type>-->")
142 
143         // assert that original sha/md5 is preserved when file is re-written.
144         val sha1 = LocalMavenRepoDownloader.digest(
145             contents = bytes,
146             fileName = "_",
147             algorithm = "SHA1"
148         ).second.toString(Charsets.UTF_8)
149         val md5 = LocalMavenRepoDownloader.digest(
150             contents = bytes,
151             fileName = "_",
152             algorithm = "MD5"
153         ).second.toString(Charsets.UTF_8)
154         assertThat(
155             fakeFileSystem.readText("external/notAndroidx/subject.pom.md5")
156         ).isEqualTo(md5)
157         assertThat(
158             fakeFileSystem.readText("internal/androidx/subject.pom.md5")
159         ).isEqualTo(md5)
160         assertThat(
161             fakeFileSystem.readText("external/notAndroidx/subject.pom.sha1")
162         ).isEqualTo(sha1)
163         assertThat(
164             fakeFileSystem.readText("internal/androidx/subject.pom.sha1")
165         ).isEqualTo(sha1)
166     }
167 
168     @Test
testKotlinArtifactsWithKmp_fetchInheritednull169     fun testKotlinArtifactsWithKmp_fetchInherited() = testKotlinArtifactsWithKmp(true)
170 
171     @Test
172     fun testKotlinArtifactsWithKmp() = testKotlinArtifactsWithKmp(false)
173 
174     private fun testKotlinArtifactsWithKmp(explicitlyFetchInherited: Boolean) {
175         ArtifactResolver.resolveArtifacts(
176             listOf("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"),
177             downloadObserver = downloader,
178             explicitlyFetchInheritedDependencies = explicitlyFetchInherited
179         )
180         val expectedKlibs = listOf(
181             "atomicfu-linuxx64/0.17.0/atomicfu-linuxx64-0.17.0.klib",
182             "atomicfu-macosarm64/0.17.0/atomicfu-macosarm64-0.17.0.klib",
183             "atomicfu-macosx64/0.17.0/atomicfu-macosx64-0.17.0.klib"
184         ).map {
185             "/external/org/jetbrains/kotlinx/$it"
186         }
187         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
188             "/external/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.1".toPath()
189                 .expectedFiles(
190                     signed = true,
191                     "kotlinx-coroutines-core-1.6.1-all.jar",
192                     "kotlinx-coroutines-core-1.6.1-sources.jar",
193                     "kotlinx-coroutines-core-1.6.1.module",
194                 ) + "/external/org/jetbrains/kotlin/kotlin-stdlib-common/1.6.0".toPath()
195                 .expectedFiles(
196                     signed = true,
197                     "kotlin-stdlib-common-1.6.0.jar",
198                     "kotlin-stdlib-common-1.6.0.pom",
199                 ) + expectedKlibs +
200                     "/external/org/jetbrains/kotlin/kotlin-stdlib-common/1.6.0/LICENSE"
201         )
202         // atomic-fu jvm is not a dependency
203         val atomicFuJvmFiles =
204             "/external/org/jetbrains/kotlinx/atomicfu-jvm/0.17.0".toPath().expectedJar(
205                 signed = true,
206                 "atomicfu-jvm-0.17.0"
207             )
208         if (explicitlyFetchInherited) {
209             assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
210                 atomicFuJvmFiles
211             )
212         } else {
213             assertThat(fakeFileSystem.allPathStrings()).doesNotContain(
214                 atomicFuJvmFiles
215             )
216         }
217     }
218 
219     @Test(expected = IllegalStateException::class)
invalidArtifactnull220     fun invalidArtifact() {
221         ArtifactResolver.resolveArtifacts(
222             listOf("this.artifact.doesnot:exists:1.0.0"),
223             downloadObserver = downloader
224         )
225     }
226 
227     @Test
testDownloadAtomicFunull228     fun testDownloadAtomicFu() {
229         ArtifactResolver.resolveArtifacts(
230             listOf("org.jetbrains.kotlinx:atomicfu:0.17.0"),
231             downloadObserver = downloader
232         )
233         val expectedKlibs = listOf(
234             "atomicfu-linuxx64/0.17.0/atomicfu-linuxx64-0.17.0.klib",
235             "atomicfu-macosarm64/0.17.0/atomicfu-macosarm64-0.17.0.klib",
236             "atomicfu-macosx64/0.17.0/atomicfu-macosx64-0.17.0.klib"
237         ).map {
238             "/external/org/jetbrains/kotlinx/$it"
239         }
240         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
241             "/external/org/jetbrains/kotlinx/atomicfu-jvm/0.17.0".toPath()
242                 .expectedJar(
243                     signed = true,
244                     "atomicfu-jvm-0.17.0"
245                 ) + expectedKlibs +
246                     "/external/org/jetbrains/kotlin/kotlin-stdlib-common/1.6.0/LICENSE"
247         )
248     }
249 
250     @Test
testSignatureFilesnull251     fun testSignatureFiles() {
252         ArtifactResolver.resolveArtifacts(
253             listOf("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1"),
254             downloadObserver = downloader
255         )
256         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
257             "/external/org/jetbrains/kotlinx/kotlinx-coroutines-test-linuxx64/1.6.1/".toPath()
258                 .expectedFiles(
259                     signed = true,
260                     "kotlinx-coroutines-test-linuxx64-1.6.1.klib",
261                     "kotlinx-coroutines-test-linuxx64-1.6.1.module",
262                 )
263         )
264     }
265 
266     @Test
testSignedArtifactWithoutKeyServerEntrynull267     fun testSignedArtifactWithoutKeyServerEntry() {
268         // https://repo1.maven.org/maven2/org/assertj/assertj-core/3.11.1/
269         // these artifacts are signed but their signature is not on any public key-server
270         // we cannot trust them so instead these builds should rely on shas
271         ArtifactResolver.resolveArtifacts(
272             listOf("org.assertj:assertj-core:3.11.1"),
273             downloadObserver = downloader
274         )
275         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
276             "/external/org/assertj/assertj-core/3.11.1/".toPath().expectedFiles(
277                 signed = true,
278                 "assertj-core-3.11.1-sources.jar",
279                 "assertj-core-3.11.1.jar",
280                 "assertj-core-3.11.1.pom",
281             )
282         )
283     }
284 
285     @Test
noArtifactsAreFetchedWhenInternalRepoIsProvidednull286     fun noArtifactsAreFetchedWhenInternalRepoIsProvided() {
287         val localRepoUris = EnvironmentConfig.supportRoot.let {
288             listOf(
289                 "file:///$it/../../prebuilts/androidx/external",
290                 "file:///$it/../../prebuilts/androidx/internal",
291             )
292         }
293         ArtifactResolver.resolveArtifacts(
294             listOf("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"),
295             downloadObserver = downloader,
296             localRepositories = localRepoUris
297         )
298         assertThat(fakeFileSystem.allPaths).isEmpty()
299     }
300 
301     @Test
testMetalavaDownloadnull302     fun testMetalavaDownload() {
303         ArtifactResolver.resolveArtifacts(
304             artifacts = listOf(
305                 "com.android.tools.metalava:metalava:1.0.0-alpha06"
306             ),
307             additionalRepositories = listOf(
308                 ArtifactResolver.createMetalavaRepo(8634556)
309             ),
310             downloadObserver = downloader
311         )
312 
313         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
314             "/external/com/android/tools/metalava/metalava/1.0.0-alpha06/".toPath().expectedJar(
315                 signed = false,
316                 "metalava-1.0.0-alpha06"
317             ).filterNot {
318                 // metalava doesn't ship sources.
319                 it.contains("sources")
320             }
321         )
322     }
323 
324     @Test
oldArtifactWithoutMetadatanull325     fun oldArtifactWithoutMetadata() {
326         ArtifactResolver.resolveArtifacts(
327             artifacts = listOf(
328                 "androidx.databinding:viewbinding:4.1.2"
329             ),
330             downloadObserver = downloader
331         )
332         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
333             "/external/androidx/databinding/viewbinding/4.1.2".toPath().expectedAar(
334                 signed = false,
335                 "viewbinding-4.1.2"
336             ).filterNot {
337                 // only pom in this artifact
338                 it.contains("module")
339             }
340         )
341     }
342 
343     @Test
testRunnernull344     fun testRunner() {
345         ArtifactResolver.resolveArtifacts(
346             artifacts = listOf(
347                 "androidx.test:runner:1.4.0"
348             ),
349             downloadObserver = downloader
350         )
351         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
352             "/internal/androidx/test/runner/1.4.0/".toPath().expectedAar(
353                 signed = false,
354                 "runner-1.4.0"
355             ).filterNot {
356                 // old artifact without metadata
357                 it.contains("module")
358             }
359         )
360     }
361 
362     /**
363      * Assert that if same artifact is referenced by two libraries but one of them uses a newer
364      * version, we fetch both versions.
365      */
366     @Test
isolateConfigurationsnull367     fun isolateConfigurations() {
368         ArtifactResolver.resolveArtifacts(
369             listOf(
370                 "androidx.room:room-runtime:2.4.2",
371                 "androidx.room:room-runtime:2.4.0",
372             ),
373             downloadObserver = downloader
374         )
375         assertThat(fakeFileSystem.allPathStrings()).containsAtLeastElementsIn(
376             "/internal/androidx/room/room-common/2.4.2/".toPath().expectedJar(
377                 signed = false,
378                 "room-common-2.4.2"
379             ) +
380                     "/internal/androidx/room/room-common/2.4.0/".toPath().expectedJar(
381                         signed = false,
382                         "room-common-2.4.0"
383                     )
384         )
385     }
386 
387     @Test
downloadWithGradlePluginSpecsnull388     fun downloadWithGradlePluginSpecs() {
389         ArtifactResolver.resolveArtifacts(
390             listOf("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0"),
391             downloadObserver = downloader
392         )
393         assertThat(fakeFileSystem.allPathStrings()).containsAtLeast(
394             "/external/org/jetbrains/kotlin/kotlin-gradle-plugin/1.7.0/" +
395                     "kotlin-gradle-plugin-1.7.0-gradle70.jar",
396             "/external/org/jetbrains/kotlin/kotlin-gradle-plugin/1.7.0/" +
397                     "kotlin-gradle-plugin-1.7.0.jar"
398         )
399     }
400 
<lambda>null401     private fun FakeFileSystem.allPathStrings() = allPaths.map {
402         it.toString()
403     }
404 
FileSystemnull405     private fun FileSystem.readText(path: String) =
406         this.openReadOnly(path.toPath()).source().buffer().use {
407             it.readUtf8()
408         }
409 
410     /**
411      * Utility method to easily create files in a given folder path
412      */
Pathnull413     private fun Path.expectedFiles(
414         signed: Boolean,
415         vararg fileNames: String,
416     ): List<String> {
417         val originals = if (signed) {
418             fileNames.flatMap {
419                 listOf(it, "$it.asc")
420             }
421         } else {
422             fileNames.toList()
423         }
424 
425         return originals.map { "$this/$it" }.flatMap {
426             listOf(it, "$it.md5", "$it.sha1")
427         }
428     }
429 
Pathnull430     private fun Path.expectedAar(
431         signed: Boolean,
432         prefix: String,
433     ) = expectedFiles(
434         signed = signed,
435         *COMMON_FILES_FOR_AARS.map {
436             "$prefix$it"
437         }.toTypedArray()
438     ) + expectedFiles(
439         // gradle doesn't need the signature for pom when there is a module file.
440         signed = false,
441         "$prefix.pom"
442     )
443 
Pathnull444     private fun Path.expectedJar(
445         signed: Boolean,
446         prefix: String,
447     ) = expectedFiles(
448         signed = signed,
449         *COMMON_FILES_FOR_JARS.map {
450             "$prefix$it"
451         }.toTypedArray()
452     ) + expectedFiles(
453         // gradle doesn't need the signature for pom when there is a module file.
454         signed = false,
455         "$prefix.pom"
456     )
457 
458     companion object {
459         val COMMON_FILES_FOR_AARS = listOf(".aar", "-sources.jar", ".module")
460         val COMMON_FILES_FOR_JARS = listOf(".jar", "-sources.jar", ".module")
461     }
462 }