// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package com.flutter.gradle import com.android.build.api.AndroidPluginVersion import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.Variant import com.flutter.gradle.DependencyVersionChecker.AGP_NAME import com.flutter.gradle.DependencyVersionChecker.GRADLE_NAME import com.flutter.gradle.DependencyVersionChecker.JAVA_NAME import com.flutter.gradle.DependencyVersionChecker.KGP_NAME import com.flutter.gradle.DependencyVersionChecker.MIN_SDK_NAME import com.flutter.gradle.DependencyVersionChecker.OUT_OF_SUPPORT_RANGE_PROPERTY import com.flutter.gradle.DependencyVersionChecker.POTENTIAL_JAVA_FIX import com.flutter.gradle.DependencyVersionChecker.errorAGPVersion import com.flutter.gradle.DependencyVersionChecker.errorGradleVersion import com.flutter.gradle.DependencyVersionChecker.errorJavaVersion import com.flutter.gradle.DependencyVersionChecker.errorKGPVersion import com.flutter.gradle.DependencyVersionChecker.errorMinSdkVersion import com.flutter.gradle.DependencyVersionChecker.getErrorMessage import com.flutter.gradle.DependencyVersionChecker.getFlavorSpecificMessage import com.flutter.gradle.DependencyVersionChecker.getPotentialAGPFix import com.flutter.gradle.DependencyVersionChecker.getPotentialGradleFix import com.flutter.gradle.DependencyVersionChecker.getPotentialKGPFix import com.flutter.gradle.DependencyVersionChecker.getPotentialSDKFix import com.flutter.gradle.DependencyVersionChecker.getWarnMessage import com.flutter.gradle.DependencyVersionChecker.warnAGPVersion import com.flutter.gradle.DependencyVersionChecker.warnGradleVersion import com.flutter.gradle.DependencyVersionChecker.warnKGPVersion import com.flutter.gradle.DependencyVersionChecker.warnMinSdkVersion import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.slot import io.mockk.verify import org.gradle.api.Action import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.logging.Logger import org.gradle.api.plugins.ExtraPropertiesExtension import org.gradle.api.tasks.TaskContainer import org.gradle.internal.extensions.core.extra import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith private const val FAKE_PROJECT_ROOT_DIR = "/fake/root/dir" // The following values will need to be modified when the corresponding "warn$DepName" versions // are updated in DependencyVersionChecker.kt // These values should match the flutter create template values. // In //packages/flutter_tools/lib/src/android/gradle_utils.dart private const val SUPPORTED_GRADLE_VERSION: String = "8.12" private val SUPPORTED_JAVA_VERSION: JavaVersion = JavaVersion.VERSION_17 private val SUPPORTED_AGP_VERSION: AndroidPluginVersion = AndroidPluginVersion(8, 9, 1) private const val SUPPORTED_KGP_VERSION: String = "2.1.0" private val SUPPORTED_SDK_VERSION: MinSdkVersion = MinSdkVersion("release", 30) class DependencyVersionCheckerTest { @Test fun `AGP version in error range results in DependencyValidationException`() { val exampleErrorAgpVersion = AndroidPluginVersion(8, 1, 0) val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(agpVersion = exampleErrorAgpVersion) val mockExtraPropertiesExtension = mockProject.extra every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit val dependencyValidationException = assertFailsWith { DependencyVersionChecker.checkDependencyVersions(mockProject) } assert( dependencyValidationException.message == getErrorMessage( AGP_NAME, exampleErrorAgpVersion.toString(), errorAGPVersion.toString(), getPotentialAGPFix(FAKE_PROJECT_ROOT_DIR) ) ) verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } } @Test fun `AGP version in warn range results in warning logs`() { val exampleWarnAgpVersion = AndroidPluginVersion(8, 2, 0) val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(agpVersion = exampleWarnAgpVersion) val mockExtraPropertiesExtension = mockProject.extra every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit val mockLogger = mockProject.logger every { mockLogger.error(any()) } returns Unit DependencyVersionChecker.checkDependencyVersions(mockProject) verify { mockLogger.error( getWarnMessage( AGP_NAME, exampleWarnAgpVersion.toString(), warnAGPVersion.toString(), getPotentialAGPFix(FAKE_PROJECT_ROOT_DIR) ) ) } verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } } @Test fun `KGP version in error range results in DependencyValidationException`() { val exampleErrorKgpVersion = "1.6.0" val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(kgpVersion = exampleErrorKgpVersion) val mockExtraPropertiesExtension = mockProject.extra every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit val dependencyValidationException = assertFailsWith { DependencyVersionChecker.checkDependencyVersions(mockProject) } println(dependencyValidationException.message) assert( dependencyValidationException.message == getErrorMessage( KGP_NAME, exampleErrorKgpVersion, errorKGPVersion.toString(), getPotentialKGPFix(FAKE_PROJECT_ROOT_DIR) ) ) verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } } @Test fun `KGP version in warn range results in warning logs`() { val exampleWarnKgpVersion = "1.8.20" val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(kgpVersion = exampleWarnKgpVersion) val mockExtraPropertiesExtension = mockProject.extra every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit val mockLogger = mockProject.logger every { mockLogger.error(any()) } returns Unit DependencyVersionChecker.checkDependencyVersions(mockProject) verify { mockLogger.error( getWarnMessage( KGP_NAME, exampleWarnKgpVersion, warnKGPVersion.toString(), getPotentialKGPFix(FAKE_PROJECT_ROOT_DIR) ) ) } verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } } // No test for Java version in warn range, as the lowest supported Java version is also the // lowest possible. @Test fun `Java version in error range results in error logs`() { val exampleErrorJavaVersion = JavaVersion.VERSION_16 val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(javaVersion = exampleErrorJavaVersion) val mockExtraPropertiesExtension = mockProject.extra every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } returns Unit val mockLogger = mockProject.logger every { mockLogger.error(any()) } returns Unit val dependencyValidationException = assertFailsWith { DependencyVersionChecker.checkDependencyVersions(mockProject) } assert( dependencyValidationException.message == getErrorMessage( JAVA_NAME, exampleErrorJavaVersion.toString(), errorJavaVersion.toString(), POTENTIAL_JAVA_FIX ) ) verify(exactly = 1) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } } @Test fun `Gradle version in error range results in DependencyValidationException`() { val exampleErrorGradleVersion = "7.0.0" val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(gradleVersion = exampleErrorGradleVersion) val mockExtraPropertiesExtension = mockProject.extra every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit val dependencyValidationException = assertFailsWith { DependencyVersionChecker.checkDependencyVersions(mockProject) } assert( dependencyValidationException.message == getErrorMessage( GRADLE_NAME, exampleErrorGradleVersion, errorGradleVersion.toString(), getPotentialGradleFix(FAKE_PROJECT_ROOT_DIR) ) ) verify { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } } @Test fun `Gradle version in warn range results in warning logs`() { val exampleWarnGradleVersion = "8.5.0" val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions(gradleVersion = exampleWarnGradleVersion) val mockExtraPropertiesExtension = mockProject.extra every { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, false) } returns Unit val mockLogger = mockProject.logger every { mockLogger.error(any()) } returns Unit DependencyVersionChecker.checkDependencyVersions(mockProject) verify { mockLogger.error( getWarnMessage( GRADLE_NAME, exampleWarnGradleVersion, warnGradleVersion.toString(), getPotentialGradleFix(FAKE_PROJECT_ROOT_DIR) ) ) } verify(exactly = 0) { mockExtraPropertiesExtension.set(OUT_OF_SUPPORT_RANGE_PROPERTY, true) } } @Test fun `min SDK version in warn range results in warning logs`() { val exampleWarnSDKVersion = 23 val flavorName1 = "flavor1" val flavorName2 = "flavor2" val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions( minSdkVersions = listOf( MinSdkVersion(flavorName1, exampleWarnSDKVersion), MinSdkVersion(flavorName2, exampleWarnSDKVersion) ) ) val mockExtraPropertiesExtension = mockProject.extra every { mockExtraPropertiesExtension.set( OUT_OF_SUPPORT_RANGE_PROPERTY, false ) } returns Unit val mockLogger = mockProject.logger every { mockLogger.error(any()) } returns Unit DependencyVersionChecker.checkDependencyVersions(mockProject) verify { mockLogger.error( getWarnMessage( getFlavorSpecificMessage(flavorName1, MIN_SDK_NAME), exampleWarnSDKVersion.toString(), warnMinSdkVersion.toString(), getPotentialSDKFix(FAKE_PROJECT_ROOT_DIR) ) ) mockLogger.error( getWarnMessage( getFlavorSpecificMessage(flavorName2, MIN_SDK_NAME), exampleWarnSDKVersion.toString(), warnMinSdkVersion.toString(), getPotentialSDKFix(FAKE_PROJECT_ROOT_DIR) ) ) } verify(exactly = 0) { mockExtraPropertiesExtension.set( OUT_OF_SUPPORT_RANGE_PROPERTY, true ) } } @Test fun `min SDK version in error range results in DependencyValidationException`() { val exampleErrorSDKVersion = 0 val flavorName = "flavor1" val mockProject = MockProjectFactory.createMockProjectWithSpecifiedDependencyVersions( minSdkVersions = listOf( MinSdkVersion(flavorName, exampleErrorSDKVersion) ) ) val mockExtraPropertiesExtension = mockProject.extra val mockLogger = mockProject.logger every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit every { mockLogger.error(any()) } returns Unit val dependencyValidationException = assertFailsWith { DependencyVersionChecker.checkDependencyVersions( mockProject ) } assert( dependencyValidationException.message == getErrorMessage( getFlavorSpecificMessage(flavorName, MIN_SDK_NAME), exampleErrorSDKVersion.toString(), errorMinSdkVersion.toString(), getPotentialSDKFix(FAKE_PROJECT_ROOT_DIR) ) ) verify(exactly = 1) { mockExtraPropertiesExtension.set( OUT_OF_SUPPORT_RANGE_PROPERTY, true ) } } @Test fun `checkMinSdkVersion throws error when in error range for min SDK version`() { val mockLogger = mockk() val mockExtraPropertiesExtension = mockk() val projectDir = "projectDir" val flavor = "flavor" val version = 0 every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit every { mockLogger.error(any()) } returns Unit val dependencyValidationException = assertFailsWith { DependencyVersionChecker.checkMinSdkVersion( minSdkVersion = MinSdkVersion(flavor, version), projectDirectory = projectDir, logger = mockLogger ) } assertEquals( dependencyValidationException.message, "Error: Your project's minimum Android SDK (flavor='flavor') version ($version) is lower than " + "Flutter's minimum supported version of $errorMinSdkVersion. Please upgrade your minimum Android SDK " + "(flavor='flavor') version. \n" + "Alternatively, use the flag \"--android-skip-build-dependency-validation\" to " + "bypass this check.\n" + "\n" + "Potential fix: Your project's minimum Android SDK version is typically defined in " + "the android block of the app-level `build.gradle(.kts)` file " + "(projectDir/app/build.gradle(.kts))." ) } @Test fun `checkMinSdkVersion logs warning when in warning range for min SDK version`() { val mockLogger = mockk() val mockExtraPropertiesExtension = mockk() val projectDir = "projectDir" val flavor = "flavor" val version = 23 every { mockExtraPropertiesExtension.set(any(), any()) } returns Unit every { mockLogger.error(any()) } returns Unit DependencyVersionChecker.checkMinSdkVersion( minSdkVersion = MinSdkVersion(flavor, version), projectDirectory = projectDir, logger = mockLogger ) val warningMessageSlot = slot() verify { mockLogger.error(capture(warningMessageSlot)) } assertEquals( warningMessageSlot.captured, "Warning: Flutter support for your project's minimum Android SDK (flavor='flavor') " + "version ($version) will soon be dropped. Please upgrade your minimum Android SDK " + "(flavor='flavor') version to a version of at least $warnMinSdkVersion soon.\n" + "Alternatively, use the flag \"--android-skip-build-dependency-validation\" to " + "bypass this check.\n" + "\n" + "Potential fix: Your project's minimum Android SDK version is typically defined in " + "the android block of the app-level `build.gradle(.kts)` file " + "(projectDir/app/build.gradle(.kts))." ) } } // There isn't a way to create a real org.gradle.api.Project object for testing unfortunately, so // these tests rely heavily on mocking. // // TODO(gmackall): We should consider adding functional tests built on top of a testing framework // perhaps like // https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit // as a way to fill this gap in testing (combined with moving functionality to individual tasks // that can be tested independently). private object MockProjectFactory { fun createMockProjectWithSpecifiedDependencyVersions( javaVersion: JavaVersion = SUPPORTED_JAVA_VERSION, gradleVersion: String = SUPPORTED_GRADLE_VERSION, agpVersion: AndroidPluginVersion = SUPPORTED_AGP_VERSION, kgpVersion: String = SUPPORTED_KGP_VERSION, minSdkVersions: List = listOf(SUPPORTED_SDK_VERSION) ): Project { // Java mockkStatic(JavaVersion::class) every { JavaVersion.current() } returns javaVersion // Gradle val mockProject = mockk() every { mockProject.gradle.gradleVersion } returns gradleVersion // AGP val mockAndroidComponentsExtension = mockk>() every { mockProject.extensions.findByType(AndroidComponentsExtension::class.java) } returns mockAndroidComponentsExtension every { mockAndroidComponentsExtension.pluginVersion } returns agpVersion // KGP every { mockProject.hasProperty(eq("kotlin_version")) } returns true every { mockProject.properties["kotlin_version"] } returns kgpVersion // Logger val mockLogger = mockk() every { mockProject.logger } returns mockLogger // Extra properties extension val mockExtraPropertiesExtension = mockk() every { mockProject.extra } returns mockExtraPropertiesExtension // Project path every { mockProject.rootDir.path } returns FAKE_PROJECT_ROOT_DIR // SDK val actionSlot = slot>() every { mockProject.afterEvaluate(capture(actionSlot)) } answers { actionSlot.captured.execute(mockProject) return@answers Unit } val onVariantsFnSlot = slot<(Variant) -> Unit>() every { mockAndroidComponentsExtension.selector() } returns mockk { every { all() } returns mockk() } every { mockProject.tasks } returns mockk { val registerTaskSlot = slot>() every { register(any(), capture(registerTaskSlot)) } answers registerAnswer@{ registerTaskSlot.captured.execute( mockk { val doLastActionSlot = slot>() every { doLast(capture(doLastActionSlot)) } answers doLastAnswer@{ doLastActionSlot.captured.execute(mockk()) return@doLastAnswer mockk() } } ) return@registerAnswer mockk() } every { named(any()) } returns mockk { every { configure(any>()) } returns mockk() } } every { mockAndroidComponentsExtension.onVariants( any(), capture(onVariantsFnSlot) ) } answers { minSdkVersions.forEach { val variant = mockk() every { variant.name } returns it.flavor every { variant.minSdk } returns mockk { every { apiLevel } returns it.version } onVariantsFnSlot.captured.invoke(variant) } return@answers Unit } return mockProject } }