android-templates/testSrc/com/android/tools/idea/templates/diff/activity/TemplateDiffTest.kt (591 lines of code) (raw):

/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.idea.templates.diff.activity import com.android.flags.junit.FlagRule import com.android.tools.idea.flags.StudioFlags import com.android.tools.idea.flags.StudioFlags.JOURNEYS_WITH_GEMINI_EXECUTION import com.android.tools.idea.flags.StudioFlags.NPW_ENABLE_NAVIGATION_UI_TEMPLATE import com.android.tools.idea.flags.StudioFlags.NPW_ENABLE_XR_TEMPLATE import com.android.tools.idea.npw.model.RenderTemplateModel import com.android.tools.idea.npw.project.DEFAULT_KOTLIN_VERSION_FOR_NEW_PROJECTS import com.android.tools.idea.npw.project.GradleAndroidModuleTemplate import com.android.tools.idea.npw.template.ModuleTemplateDataBuilder import com.android.tools.idea.npw.template.ProjectTemplateDataBuilder import com.android.tools.idea.npw.template.TemplateResolver import com.android.tools.idea.templates.diff.TemplateDiffTestUtils.getPinnedAgpVersion import com.android.tools.idea.testing.AndroidGradleProjectRule import com.android.tools.idea.testing.AndroidProjectRule import com.android.tools.idea.wizard.template.BooleanParameter import com.android.tools.idea.wizard.template.Category import com.android.tools.idea.wizard.template.FormFactor import com.android.tools.idea.wizard.template.Language import com.android.tools.idea.wizard.template.StringParameter import com.intellij.openapi.project.Project import com.intellij.testFramework.DisposableRule import kotlin.system.measureTimeMillis import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.runners.Parameterized.Parameters /** * Template test that generates the template files and diffs them against golden files located in * android-templates/testData/golden * * For context and instructions on running and generating golden files, see go/template-diff-tests */ @RunWith(Parameterized::class) class TemplateDiffTest(private val testMode: TestMode) { @get:Rule val projectRule: TestRule = if (shouldUseGradle()) AndroidGradleProjectRule(agpVersionSoftwareEnvironment = getPinnedAgpVersion()) else AndroidProjectRule.withAndroidModels() @get:Rule val disposableRule = DisposableRule() @get:Rule val xrTemplateFlagRule = FlagRule(NPW_ENABLE_XR_TEMPLATE, true) @get:Rule val navigationFlagRule = FlagRule(NPW_ENABLE_NAVIGATION_UI_TEMPLATE, true) @get:Rule val journeyFlagRule = FlagRule(JOURNEYS_WITH_GEMINI_EXECUTION, true) companion object { /** Keeps track of whether the previous parameterized test failed */ private var validationFailed = false /** * Utilizes parameterized test to decide which modes to run the test in. When DIFFING the * template-generated files against golden files, we do not run Gradle sync, to keep the test * fast. * * When we need to validate and generate the golden files however, we run the first part, * VALIDATING, with Gradle sync, which calls into [GoldenFileValidator] that also builds and * Lints. Then, after the template is validated, we generate the golden files WITHOUT Gradle * sync, to have them be diff-able without syncing. */ @JvmStatic @Parameters(name = "{0}") fun data(): List<TestMode> { return if (shouldGenerateGolden()) { listOf(TestMode.VALIDATING, TestMode.GENERATING) } else { listOf(TestMode.DIFFING) } } /** * Gets the system property for whether to generate and overwrite the golden files. This can be * run from Bazel with the option: --test_env=GENERATE_GOLDEN=true */ private fun shouldGenerateGolden(): Boolean { return System.getenv("GENERATE_GOLDEN")?.equals("true") ?: false } /** * Gets the system property for whether to fail the test suite on first error with "previous * validation failed" or to run all of the validation tests. The former is enabled by default * and could be more useful for people not expecting errors, since te full suite takes a long * time (~45+ minutes) to run. The latter is more useful for people making large-scale changes * who want to fix more errors at once. */ private fun shouldFailEarly(): Boolean { return !(System.getenv("RUN_FULL_VALIDATION")?.equals("true") ?: false) } } @Before fun setUp() { // By default, this makes the suite fail early if there is a validation error. If // RUN_FULL_VALIDATION is set, then all validation is run, but golden generation is still // cancelled if (shouldFailEarly() || testMode == TestMode.GENERATING) { assertFalse("Previous validation failed", validationFailed) } println("Current test mode: $testMode") if (testMode != TestMode.DIFFING) { validationFailed = true } getPinnedAgpVersion().agpVersion?.let { StudioFlags.AGP_VERSION_TO_USE.override(it) } } @After fun tearDown() { StudioFlags.AGP_VERSION_TO_USE.clearOverride() StudioFlags.NPW_ENABLE_NAVIGATION_UI_TEMPLATE.clearOverride() } enum class TestMode { DIFFING, VALIDATING, GENERATING, } private fun shouldUseGradle(): Boolean { return when (testMode) { TestMode.DIFFING -> false TestMode.VALIDATING -> true TestMode.GENERATING -> false } } /** * Checks the given template in the given category. Supports overridden template values. * * @param name the template name * @param customizers An instance of [ProjectStateCustomizer]s used for providing template and * project overrides. */ private fun checkCreateTemplate( name: String, vararg customizers: ProjectStateCustomizer, templateStateCustomizer: TemplateStateCustomizer = mapOf(), category: Category? = null, formFactor: FormFactor? = null, ) { val template = TemplateResolver.getTemplateByName(name, category, formFactor)!! val goldenDirName = findEnclosingTestMethodName() templateStateCustomizer.forEach { (parameterName: String, overrideValue: Any) -> val parameter = template.parameters.find { it.name == parameterName }!! when (parameter) { is BooleanParameter -> parameter.value = overrideValue as Boolean is StringParameter -> parameter.value = overrideValue as String else -> { throw IllegalStateException() } } } val msToCheck = measureTimeMillis { val project: Project = getProject() val projectRenderer: ProjectRenderer = when (testMode) { TestMode.DIFFING -> ProjectDiffer(template, goldenDirName) TestMode.VALIDATING -> GoldenFileValidator(template, goldenDirName, projectRule as AndroidGradleProjectRule) TestMode.GENERATING -> GoldenFileGenerator(template, goldenDirName) } // TODO: We need to check more combinations of different moduleData/template params here. // Running once to make it as easy as possible. projectRenderer.renderProject(project, getPinnedAgpVersion(), *customizers) if (testMode == TestMode.GENERATING) { printUnzipInstructions() } } println("Checked $name ($goldenDirName) successfully in ${msToCheck}ms\n") validationFailed = false } private fun getProject() = if (shouldUseGradle()) { (projectRule as AndroidGradleProjectRule).project } else { (projectRule as AndroidProjectRule).project } /** * Goes up the stack trace to find the closest @Test method that this was called from. This will * be used as a unique identifier for the golden directory name */ private fun findEnclosingTestMethodName(): String { val stackTrace = Thread.currentThread().stackTrace for (i in 2..stackTrace.size) { val element = stackTrace[i] val methodName = element.methodName val clazz = Class.forName(element.className) try { val method = clazz.getDeclaredMethod(methodName) if (method.getAnnotation(Test::class.java) != null) { println("Using @Test method name: $methodName") return methodName } } catch (_: NoSuchMethodException) { // Kt methods with optional parameters don't seem to play well } } throw RuntimeException("Must be called from a @Test") } private fun withKotlin( kotlinVersion: String = DEFAULT_KOTLIN_VERSION_FOR_NEW_PROJECTS ): ProjectStateCustomizer = { _: ModuleTemplateDataBuilder, projectData: ProjectTemplateDataBuilder -> projectData.language = Language.Kotlin // Use the Kotlin version for tests projectData.kotlinVersion = kotlinVersion } private val withSpecificKotlin: ProjectStateCustomizer = withKotlin(RenderTemplateModel.getComposeKotlinVersion()) @Suppress("SameParameterValue") private fun withApplicationId(applicationId: String): ProjectStateCustomizer = { _: ModuleTemplateDataBuilder, projectData: ProjectTemplateDataBuilder -> projectData.applicationPackage = applicationId } @Suppress("SameParameterValue") private fun withPackage(packageName: String): ProjectStateCustomizer = { moduleData: ModuleTemplateDataBuilder, projectData: ProjectTemplateDataBuilder -> moduleData.packageName = packageName val paths = GradleAndroidModuleTemplate.createDefaultModuleTemplate(getProject(), moduleData.name!!) .paths moduleData.setModuleRoots(paths, projectData.topOut!!.path, moduleData.name!!, packageName) } private fun printUnzipInstructions() { println("\n----------------------------------------") println( "Outputting generated golden and Lint baseline files to undeclared outputs\n\n" + "To update these files, unzip golden/ and lintBaseline/ from outputs.zip to the android-templates/testData/golden directory.\n" + "For a remote invocation, download and unzip golden/ and lintBaseline/ from outputs.zip:\n" + " unzip outputs.zip \"golden/*\" -d \"$(bazel info workspace)/tools/adt/idea/android-templates/testData/\"\n" + " unzip outputs.zip \"lintBaseline/*\" -d \"$(bazel info workspace)/tools/adt/idea/android-templates/testData/\"\n" + "\n" + "For a local invocation, outputs.zip will be in bazel-testlogs:\n" + " unzip $(bazel info bazel-testlogs)/tools/adt/idea/android-templates/intellij.android.templates.tests_tests__TemplateDiffTest/test.outputs/outputs.zip \\\n" + " \"golden/*\" -d \"$(bazel info workspace)/tools/adt/idea/android-templates/testData/\"\n" + " unzip $(bazel info bazel-testlogs)/tools/adt/idea/android-templates/intellij.android.templates.tests_tests__TemplateDiffTest/test.outputs/outputs.zip \\\n" + " \"lintBaseline/*\" -d \"$(bazel info workspace)/tools/adt/idea/android-templates/testData/\"" ) println("----------------------------------------\n") } /* * Tests for individual activity templates go below here. Each test method should only test one * template parameter combination, because the test method name is used as the directory name for * the golden files. */ @Test fun testNewEmptyViewsActivity() { checkCreateTemplate("Empty Views Activity") } @Test fun testNewEmptyViewsActivity_notInRootPackage() { checkCreateTemplate( "Empty Views Activity", withApplicationId("com.mycompany.myapp"), withPackage("com.mycompany.myapp.subpackage"), ) } @Test fun testNewEmptyViewsActivityKotlin() { checkCreateTemplate("Empty Views Activity", withKotlin()) } @Test fun testNewEmptyViewsActivityKotlin_notInRootPackage() { checkCreateTemplate( "Empty Views Activity", withKotlin(), withApplicationId("com.mycompany.myapp"), withPackage("com.mycompany.myapp.subpackage"), ) } @Test fun testNewBasicViewsActivity() { checkCreateTemplate("Basic Views Activity") } @Test fun testNewBasicViewsActivityWithKotlin() { checkCreateTemplate("Basic Views Activity", withKotlin()) } @Test fun testNewViewModelActivity() { checkCreateTemplate("Fragment + ViewModel") } @Test fun testNewViewModelActivityWithKotlin() { checkCreateTemplate("Fragment + ViewModel", withKotlin()) } @Test fun testNewTabbedActivity() { checkCreateTemplate("Tabbed Views Activity") } @Test fun testNewTabbedActivityWithKotlin() { checkCreateTemplate("Tabbed Views Activity", withKotlin()) } @Test fun testNewNavigationDrawerActivity() { StudioFlags.NPW_ENABLE_NAVIGATION_UI_TEMPLATE.override(false) checkCreateTemplate("Navigation Drawer Views Activity") } @Test fun testNewNavigationDrawerActivityWithKotlin() { StudioFlags.NPW_ENABLE_NAVIGATION_UI_TEMPLATE.override(false) checkCreateTemplate("Navigation Drawer Views Activity", withKotlin()) } @Test fun testNewPrimaryDetailFlow() { checkCreateTemplate("Primary/Detail Views Flow") } @Test fun testNewPrimaryDetailFlowWithKotlin() { checkCreateTemplate("Primary/Detail Views Flow", withKotlin()) } @Test fun testNewFullscreenActivity() { checkCreateTemplate("Fullscreen Views Activity") } @Test fun testNewFullscreenActivityWithKotlin() { checkCreateTemplate("Fullscreen Views Activity", withKotlin()) } @Test fun testNewFullscreenActivity_activityNotInRootPackage() { checkCreateTemplate( "Fullscreen Views Activity", withApplicationId("com.mycompany.myapp"), withPackage("com.mycompany.myapp.subpackage"), ) } @Test fun testNewFullscreenActivityWithKotlin_activityNotInRootPackage() { checkCreateTemplate( "Fullscreen Views Activity", withKotlin(), withApplicationId("com.mycompany.myapp"), withPackage("com.mycompany.myapp.subpackage"), ) } @Test fun testNewLoginActivity() { checkCreateTemplate("Login Views Activity") } @Test fun testNewLoginActivityWithKotlin() { checkCreateTemplate("Login Views Activity", withKotlin()) } @Test fun testNewScrollingActivity() { checkCreateTemplate("Scrolling Views Activity") } @Test fun testNewScrollingActivityWithKotlin() { checkCreateTemplate("Scrolling Views Activity", withKotlin()) } @Test fun testNewSettingsActivity() { checkCreateTemplate("Settings Views Activity") } @Test fun testNewSettingsActivityWithKotlin() { checkCreateTemplate("Settings Views Activity", withKotlin()) } @Test fun testNewSettingsActivityMultipleScreens() { checkCreateTemplate( "Settings Views Activity", templateStateCustomizer = mapOf("Split settings hierarchy into separate sub-screens" to true), ) } @Test fun testNewSettingsActivityWithKotlinMultipleScreens() { checkCreateTemplate( "Settings Views Activity", withKotlin(), templateStateCustomizer = mapOf("Split settings hierarchy into separate sub-screens" to true), ) } @Test fun testBottomNavigationActivity() { StudioFlags.NPW_ENABLE_NAVIGATION_UI_TEMPLATE.override(false) checkCreateTemplate("Bottom Navigation Views Activity") } @Test fun testBottomNavigationActivityWithKotlin() { StudioFlags.NPW_ENABLE_NAVIGATION_UI_TEMPLATE.override(false) checkCreateTemplate("Bottom Navigation Views Activity", withKotlin()) } @Test fun testGoogleAdMobAdsActivity() { checkCreateTemplate("Google AdMob Ads Views Activity") } @Test fun testGoogleAdMobAdsActivityWithKotlin() { checkCreateTemplate("Google AdMob Ads Views Activity", withKotlin()) } @Test fun testGoogleMapsActivity() { checkCreateTemplate("Google Maps Views Activity") } @Test fun testGoogleMapsActivityWithKotlin() { checkCreateTemplate("Google Maps Views Activity", withKotlin()) } @Test fun testGooglePayActivity() { checkCreateTemplate("Google Pay Views Activity") } @Test fun testGooglePayActivityWithKotlin() { checkCreateTemplate("Google Pay Views Activity", withKotlin()) } @Test fun testGoogleWalletActivity() { checkCreateTemplate("Google Wallet Activity") } @Test fun testGoogleWalletActivityWithKotlin() { checkCreateTemplate("Google Wallet Activity", withKotlin()) } @Test fun testGameActivity() { checkCreateTemplate("Game Activity (C++)") } @Test fun testGameActivityWithKotlin() { checkCreateTemplate("Game Activity (C++)", withKotlin()) } @Test fun testComposeActivityMaterial3() { checkCreateTemplate("Empty Activity", withSpecificKotlin) // Compose is always Kotlin } @Test fun testComposeNavigationUiActivityMaterial3() { checkCreateTemplate("Navigation UI Activity", withSpecificKotlin) // Compose is always Kotlin } @Test fun testResponsiveActivity() { checkCreateTemplate("Responsive Views Activity") } @Test fun testResponsiveActivityWithKotlin() { checkCreateTemplate("Responsive Views Activity", withKotlin()) } @Test fun testNewComposeWearActivity() { checkCreateTemplate("Empty Wear App", withSpecificKotlin) } @Test fun testNewComposeWearActivityWithTileAndComplication() { checkCreateTemplate("Empty Wear App With Tile And Complication", withSpecificKotlin) } @Test fun testNewTvActivity() { checkCreateTemplate("Android TV Blank Views Activity") } @Test fun testNewTvActivityWithKotlin() { checkCreateTemplate("Android TV Blank Views Activity", withKotlin()) } @Test fun testNewEmptyComposeForTvActivity() { checkCreateTemplate("Empty Activity", withSpecificKotlin, formFactor = FormFactor.Tv) } @Test fun testNewNativeCppActivity() { checkCreateTemplate("Native C++") } @Test fun testNewNativeCppActivityWithKotlin() { checkCreateTemplate("Native C++", withKotlin()) } /* * Tests for individual fragment templates go below here. Each test method should only test one * template parameter combination, because the test method name is used as the directory name for * the golden files. */ @Test fun testNewListFragment() { checkCreateTemplate("Fragment (List)") } @Test fun testNewListFragmentWithKotlin() { checkCreateTemplate("Fragment (List)", withKotlin()) } @Test fun testNewModalBottomSheet() { checkCreateTemplate("Modal Bottom Sheet") } @Test fun testNewModalBottomSheetWithKotlin() { checkCreateTemplate("Modal Bottom Sheet", withKotlin()) } @Test fun testNewBlankFragment() { checkCreateTemplate("Fragment (Blank)") } @Test fun testNewBlankFragmentWithKotlin() { checkCreateTemplate("Fragment (Blank)", withKotlin()) } @Test fun testNewSettingsFragment() { checkCreateTemplate("Settings Fragment") } @Test fun testNewSettingsFragmentWithKotlin() { checkCreateTemplate("Settings Fragment", withKotlin()) } @Test fun testNewViewModelFragment() { checkCreateTemplate("Fragment (with ViewModel)") } @Test fun testNewViewModelFragmentWithKotlin() { checkCreateTemplate("Fragment (with ViewModel)", withKotlin()) } @Test fun testNewScrollingFragment() { checkCreateTemplate("Scrolling Fragment") } @Test fun testNewScrollingFragmentWithKotlin() { checkCreateTemplate("Scrolling Fragment", withKotlin()) } @Test fun testNewFullscreenFragment() { checkCreateTemplate("Fullscreen Fragment") } @Test fun testNewFullscreenFragmentWithKotlin() { checkCreateTemplate("Fullscreen Fragment", withKotlin()) } @Test fun testNewGoogleMapsFragment() { checkCreateTemplate("Google Maps Fragment") } @Test fun testNewGoogleMapsFragmentWithKotlin() { checkCreateTemplate("Google Maps Fragment", withKotlin()) } @Test fun testNewGoogleAdMobFragment() { checkCreateTemplate("Google AdMob Ads Fragment") } @Test fun testNewGoogleAdMobFragmentWithKotlin() { checkCreateTemplate("Google AdMob Ads Fragment", withKotlin()) } @Test fun testLoginFragment() { checkCreateTemplate("Login Fragment") } @Test fun testLoginFragmentWithKotlin() { checkCreateTemplate("Login Fragment", withKotlin()) } /* * Tests for individual miscellaneous templates go below here. Each test method should only test * one template parameter combination, because the test method name is used as the directory name * for the golden files. */ @Test fun testNewAppWidget() { checkCreateTemplate("App Widget") } @Test fun testNewBroadcastReceiver() { checkCreateTemplate("Broadcast Receiver") } @Test fun testNewBroadcastReceiverWithKotlin() { checkCreateTemplate("Broadcast Receiver", withKotlin()) } @Test fun testNewContentProvider() { checkCreateTemplate("Content Provider") } @Test fun testNewContentProviderWithKotlin() { checkCreateTemplate("Content Provider", withKotlin()) } @Test fun testNewSliceProvider() { checkCreateTemplate("Slice Provider") } @Test fun testNewSliceProviderWithKotlin() { checkCreateTemplate("Slice Provider", withKotlin()) } @Test fun testNewCustomView() { checkCreateTemplate("Custom View") } @Test fun testNewIntentService() { checkCreateTemplate("Service (IntentService)") } @Test fun testNewIntentServiceWithKotlin() { checkCreateTemplate("Service (IntentService)", withKotlin()) } @Test fun testNewService() { checkCreateTemplate("Service") } @Test fun testNewServiceWithKotlin() { checkCreateTemplate("Service", withKotlin()) } @Test fun testAndroidManifest() { checkCreateTemplate("Android Manifest File") } @Test fun testNewAidlFile() { checkCreateTemplate("AIDL File") } @Test fun testNewLayoutXmlFile() { checkCreateTemplate("Layout XML File") } @Test fun testNewValuesXmlFile() { checkCreateTemplate("Values XML File") } @Test fun testNewShortcutsXmlFile() { checkCreateTemplate("Shortcuts XML File") } @Test fun testAutomotiveMessagingService() { checkCreateTemplate("Messaging Service") } @Test fun testAutomotiveMessagingServiceWithKotlin() { checkCreateTemplate("Messaging Service", withKotlin()) } @Test fun testAutomotiveMediaService() { checkCreateTemplate("Media Service") } @Test fun testAutomotiveMediaServiceWithKotlin() { checkCreateTemplate("Media Service", withKotlin()) } @Ignore("b/418047552") @Test fun testXRBasicHeadsetActivity() { checkCreateTemplate("Basic Headset Activity", withSpecificKotlin) } @Test fun testJourneysFile() { checkCreateTemplate("Journey File") } } typealias TemplateStateCustomizer = Map<String, Any> typealias ProjectStateCustomizer = (ModuleTemplateDataBuilder, ProjectTemplateDataBuilder) -> Unit