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 }