-
-
Notifications
You must be signed in to change notification settings - Fork 24
task/ADFA-3961 avd generate cache phase #1320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jomen-adfa
wants to merge
7
commits into
stage
Choose a base branch
from
task/ADFA-3961-avd-generate-cache
base: stage
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
12739d9
add 5 templates and include assemble* tasks then copy maven cache to …
jomen-adfa f78fef7
add first 7 template projects created with kotlin project language
jomen-adfa 590bbcf
add tasks to convert cache to repo and to recompress the repo for br
jomen-adfa c66bfc0
account for removal of onboarding screen
jomen-adfa 3203b06
fine tune
jomen-adfa 53bb953
constant definitions
jomen-adfa ca0271d
export gradle-api-*.jar
jomen-adfa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
239 changes: 239 additions & 0 deletions
239
app/src/androidTest/kotlin/com/itsaky/androidide/AutomationEndToEndTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| package com.itsaky.androidide | ||
|
|
||
| import androidx.test.core.app.ActivityScenario | ||
| import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled | ||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import androidx.test.uiautomator.UiSelector | ||
| import com.itsaky.androidide.activities.SplashActivity | ||
| import com.itsaky.androidide.helper.advancePastWelcomeScreen | ||
| import com.itsaky.androidide.helper.clickFirstAccessibilityNodeByText | ||
| import com.itsaky.androidide.helper.ensureOnHomeScreenBeforeCreateProject | ||
| import com.itsaky.androidide.helper.grantAllRequiredPermissionsThroughOnboardingUi | ||
| import com.itsaky.androidide.helper.initializeProjectRunAssembleTasksAndCancelBuild | ||
| import com.itsaky.androidide.helper.selectProjectTemplate | ||
| import com.itsaky.androidide.helper.waitForMainHomeOrEditorUi | ||
| import com.itsaky.androidide.resources.R as ResourcesR | ||
| import com.itsaky.androidide.screens.HomeScreen.clickCreateProjectHomeScreen | ||
| import com.itsaky.androidide.screens.OnboardingScreen | ||
| import com.itsaky.androidide.screens.ProjectSettingsScreen.clickCreateProjectProjectSettings | ||
| import com.itsaky.androidide.screens.ProjectSettingsScreen.selectKotlinLanguage | ||
| import com.itsaky.androidide.screens.ProjectSettingsScreen.setProjectName | ||
| import com.itsaky.androidide.screens.PermissionScreen | ||
| import com.itsaky.androidide.utils.PermissionsHelper | ||
| import com.kaspersky.kaspresso.testcases.api.testcase.TestCase | ||
| import org.junit.Assert.assertEquals | ||
| import org.junit.Assert.assertFalse | ||
| import org.junit.Assert.assertTrue | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
|
|
||
| private const val LAUNCH_SETTLE_DELAY_MS = 1_000L | ||
| private const val PRIVACY_DIALOG_TIMEOUT_MS = 2_000L | ||
| private const val PERMISSIONS_ASSERTION_TIMEOUT_MS = 3_000L | ||
| private const val KOTLIN_LANGUAGE_TEMPLATE_COUNT = 7 | ||
|
|
||
| /** | ||
| * Single continuous E2E test that drives the app from first launch through | ||
| * onboarding, project creation, builds, and beyond. | ||
| * | ||
| * The activity launches once and stays alive. Each stage is a Kaspresso | ||
| * `step()` so failures report exactly which stage broke. | ||
| */ | ||
| @RunWith(AndroidJUnit4::class) | ||
| class AutomationEndToEndTest : TestCase() { | ||
|
|
||
| private val targetContext | ||
| get() = InstrumentationRegistry.getInstrumentation().targetContext | ||
|
|
||
| private val acceptText: String | ||
| get() = targetContext.getString(ResourcesR.string.privacy_disclosure_accept) | ||
|
|
||
| private val learnMoreText: String | ||
| get() = targetContext.getString(ResourcesR.string.privacy_disclosure_learn_more) | ||
|
|
||
| private val dialogTitle: String | ||
| get() = targetContext.getString(ResourcesR.string.privacy_disclosure_title) | ||
|
|
||
| @Test | ||
| fun test_endToEnd() = run { | ||
|
|
||
| // ── Launch ── | ||
|
|
||
| step("Launch app") { | ||
| ActivityScenario.launch(SplashActivity::class.java) | ||
| Thread.sleep(LAUNCH_SETTLE_DELAY_MS) | ||
| } | ||
|
|
||
| // ── Welcome Screen ── | ||
|
|
||
| step("Verify welcome screen") { | ||
| OnboardingScreen { | ||
| greetingTitle.isVisible() | ||
| greetingSubtitle.isVisible() | ||
| nextButton { | ||
| isVisible() | ||
| isClickable() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| advancePastWelcomeScreen() | ||
|
|
||
| // ── Permissions Screen (with privacy disclosure dialog overlay) ── | ||
|
|
||
| step("Verify privacy disclosure dialog") { | ||
| val d = device.uiDevice | ||
| val title = d.findObject(UiSelector().text(dialogTitle)) | ||
| assertTrue("Dialog title missing", title.waitForExists(PRIVACY_DIALOG_TIMEOUT_MS)) | ||
| assertTrue("Accept button missing", d.findObject(UiSelector().text(acceptText)).exists()) | ||
| assertTrue("Learn more button missing", d.findObject(UiSelector().text(learnMoreText)).exists()) | ||
| } | ||
|
|
||
| step("Accept privacy disclosure") { | ||
| clickFirstAccessibilityNodeByText(acceptText) | ||
| device.uiDevice.waitForIdle() | ||
| } | ||
|
|
||
| step("Verify privacy dialog does not reappear") { | ||
| assertFalse( | ||
| "Dialog should not reappear", | ||
| device.uiDevice.findObject(UiSelector().text(dialogTitle)).exists(), | ||
| ) | ||
| } | ||
|
|
||
| val required = PermissionsHelper.getRequiredPermissions(targetContext) | ||
|
|
||
| step("Verify all permission items") { | ||
| flakySafely(timeoutMs = PERMISSIONS_ASSERTION_TIMEOUT_MS) { | ||
| PermissionScreen { | ||
| title { isVisible() } | ||
| subTitle { isVisible() } | ||
| rvPermissions { | ||
| isVisible() | ||
| isDisplayed() | ||
| } | ||
| assertEquals(required.size, rvPermissions.getSize()) | ||
|
|
||
| rvPermissions { | ||
| required.forEachIndexed { index, item -> | ||
| childAt<PermissionScreen.PermissionItem>(index) { | ||
| title { | ||
| isVisible() | ||
| hasText(item.title) | ||
| } | ||
| description { | ||
| isVisible() | ||
| hasText(item.description) | ||
| } | ||
| grantButton { | ||
| isVisible() | ||
| isClickable() | ||
| hasText(R.string.title_grant) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| grantAllRequiredPermissionsThroughOnboardingUi() | ||
|
|
||
| step("Confirm all permissions granted") { | ||
| flakySafely(timeoutMs = PERMISSIONS_ASSERTION_TIMEOUT_MS) { | ||
| assertTrue(PermissionsHelper.areAllPermissionsGranted(targetContext)) | ||
| } | ||
| } | ||
|
|
||
| step("Confirm all grant buttons disabled") { | ||
| device.uiDevice.waitForIdle() | ||
| PermissionScreen { | ||
| rvPermissions { | ||
| required.indices.forEach { index -> | ||
| childAt<PermissionScreen.PermissionItem>(index) { | ||
| grantButton { | ||
| isNotEnabled() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| step("Tap Finish installation") { | ||
| // The button is in the gesture exclusion zone — use accessibility click | ||
| clickFirstAccessibilityNodeByText("Finish installation") | ||
| } | ||
|
|
||
| step("Wait for IDE setup to complete") { | ||
| waitForMainHomeOrEditorUi(device.uiDevice) | ||
| } | ||
|
|
||
| // ── Phase 2: Project creation + build across default and Kotlin template variants ── | ||
|
|
||
| ensureOnHomeScreenBeforeCreateProject() | ||
|
|
||
| data class TemplateConfig( | ||
| val label: String, | ||
| val templateResId: Int, | ||
| val projectName: String, | ||
| val visibleLabelOverride: String? = null, | ||
| val useKotlinLanguage: Boolean = false, | ||
| ) | ||
|
|
||
| val defaultLanguageTemplates = listOf( | ||
| TemplateConfig("No Activity", R.string.template_no_activity, "TestNoActivity"), | ||
| TemplateConfig("Empty Activity", R.string.template_empty, "TestEmptyActivity"), | ||
| TemplateConfig("Basic Activity", R.string.template_basic, "TestBasicActivity"), | ||
| TemplateConfig( | ||
| "Navigation Drawer", | ||
| R.string.template_navigation_drawer, | ||
| "TestNavigationDrawer", | ||
| visibleLabelOverride = "Navigation Drawer", | ||
| ), | ||
| TemplateConfig("Bottom Nav Activity", R.string.template_navigation_tabs, "TestBottomNavActivity"), | ||
| TemplateConfig( | ||
| "No AndroidX", | ||
| R.string.template_no_AndroidX, | ||
| "TestNoAndroidX", | ||
| visibleLabelOverride = "No AndroidX", | ||
| ), | ||
| TemplateConfig("Tabbed Activity", R.string.template_tabs, "TestTabbedActivity"), | ||
| TemplateConfig("Compose Activity", R.string.template_compose, "TestComposeActivity"), | ||
| ) | ||
|
|
||
| val kotlinLanguageTemplates = defaultLanguageTemplates.take(KOTLIN_LANGUAGE_TEMPLATE_COUNT).map { config -> | ||
| config.copy( | ||
| label = "Kotlin ${config.label}", | ||
| projectName = "Kt${config.projectName}", | ||
| useKotlinLanguage = true, | ||
| ) | ||
| } | ||
|
|
||
| val templates = defaultLanguageTemplates + kotlinLanguageTemplates | ||
|
|
||
| for ((index, config) in templates.withIndex()) { | ||
| step("Create+build template ${index + 1}/${templates.size}: ${config.label}") { | ||
| clickCreateProjectHomeScreen() | ||
| } | ||
| selectProjectTemplate( | ||
| "Select ${config.label} template", | ||
| config.templateResId, | ||
| config.visibleLabelOverride, | ||
| ) | ||
| if (config.useKotlinLanguage) { | ||
| selectKotlinLanguage() | ||
| } | ||
| setProjectName(config.projectName) | ||
| clickCreateProjectProjectSettings() | ||
| initializeProjectRunAssembleTasksAndCancelBuild() | ||
|
|
||
| if (index < templates.lastIndex) { | ||
| ensureOnHomeScreenBeforeCreateProject() | ||
| } | ||
| } | ||
|
|
||
| // ── Future phases (preferences, more templates, etc.) go here ── | ||
| } | ||
| } | ||
12 changes: 12 additions & 0 deletions
12
app/src/androidTest/kotlin/com/itsaky/androidide/AutomationTestSuite.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.itsaky.androidide | ||
|
|
||
| import org.junit.runner.RunWith | ||
| import org.junit.runners.Suite | ||
|
|
||
| @RunWith(Suite::class) | ||
| @Suite.SuiteClasses( | ||
| CleanupTest::class, | ||
| AutomationEndToEndTest::class, | ||
| ExportCacheDirectoryTest::class, | ||
| ) | ||
| class AutomationTestSuite |
98 changes: 98 additions & 0 deletions
98
app/src/androidTest/kotlin/com/itsaky/androidide/ExportCacheDirectoryTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package com.itsaky.androidide | ||
|
|
||
| import android.os.Environment | ||
| import androidx.test.ext.junit.runners.AndroidJUnit4 | ||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import org.junit.Assume.assumeTrue | ||
| import org.junit.Assert.assertTrue | ||
| import org.junit.Test | ||
| import org.junit.runner.RunWith | ||
| import java.io.File | ||
|
|
||
| @RunWith(AndroidJUnit4::class) | ||
| class ExportCacheDirectoryTest { | ||
|
|
||
| @Test | ||
| fun exportGradleModuleCacheBeforeConnectedTestCleanup() { | ||
| val context = InstrumentationRegistry.getInstrumentation().targetContext | ||
| val args = InstrumentationRegistry.getArguments() | ||
|
|
||
| val destinationRelativePath = | ||
| args.getString(ARG_DESTINATION_RELATIVE_PATH) ?: DEFAULT_DESTINATION_RELATIVE_PATH | ||
|
|
||
| val source = File(context.filesDir, SOURCE_RELATIVE_PATH) | ||
| val externalBase = File(Environment.getExternalStorageDirectory(), EXPORT_BASE_DIRECTORY).canonicalFile | ||
| val destination = resolveSafeDestination(externalBase, destinationRelativePath) | ||
|
|
||
| assumeTrue("Source does not exist: ${source.absolutePath}", source.exists()) | ||
| assumeTrue("Source is not a directory: ${source.absolutePath}", source.isDirectory) | ||
|
|
||
| destination.deleteRecursively() | ||
| destination.parentFile?.mkdirs() | ||
|
|
||
| assertTrue( | ||
| "Failed to copy ${source.absolutePath} to ${destination.absolutePath}", | ||
| source.copyRecursively(target = destination, overwrite = true), | ||
| ) | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| assertTrue("Destination does not exist: ${destination.absolutePath}", destination.exists()) | ||
| } | ||
|
|
||
| @Test | ||
| fun exportGeneratedGradleApiJarWhenRequested() { | ||
| val context = InstrumentationRegistry.getInstrumentation().targetContext | ||
| val args = InstrumentationRegistry.getArguments() | ||
| val version = args.getString(ARG_GRADLE_API_VERSION) | ||
|
|
||
| assumeTrue("No Gradle API version requested", !version.isNullOrBlank()) | ||
| require(!version!!.contains(File.separatorChar)) { | ||
| "Gradle API version must not contain path separators: $version" | ||
| } | ||
|
|
||
| val destinationRelativePath = | ||
| args.getString(ARG_GRADLE_API_DESTINATION_RELATIVE_PATH) | ||
| ?: DEFAULT_GRADLE_API_DESTINATION_RELATIVE_PATH | ||
|
|
||
| val source = | ||
| File( | ||
| context.filesDir, | ||
| "$GRADLE_CACHE_RELATIVE_PATH/$version/generated-gradle-jars/gradle-api-$version.jar", | ||
| ) | ||
| val externalBase = File(Environment.getExternalStorageDirectory(), EXPORT_BASE_DIRECTORY).canonicalFile | ||
| val destinationDir = resolveSafeDestination(externalBase, destinationRelativePath) | ||
| val destination = File(destinationDir, source.name) | ||
|
|
||
| assertTrue("Source does not exist: ${source.absolutePath}", source.exists()) | ||
| assertTrue("Source is not a file: ${source.absolutePath}", source.isFile) | ||
|
|
||
| destination.parentFile?.mkdirs() | ||
| source.copyTo(target = destination, overwrite = true) | ||
|
|
||
| assertTrue("Destination does not exist: ${destination.absolutePath}", destination.exists()) | ||
| } | ||
|
|
||
| private fun resolveSafeDestination(base: File, relativePath: String): File { | ||
| require(relativePath.isNotBlank()) { | ||
| "Destination path must be a non-blank relative path" | ||
| } | ||
| require(!File(relativePath).isAbsolute) { | ||
| "Destination path must be relative" | ||
| } | ||
|
|
||
| val destination = File(base, relativePath.removePrefix("$EXPORT_BASE_DIRECTORY/")).canonicalFile | ||
| require(destination.toPath().startsWith(base.toPath())) { | ||
| "Destination must stay under ${base.absolutePath}: ${destination.absolutePath}" | ||
| } | ||
| return destination | ||
| } | ||
|
|
||
| private companion object { | ||
| const val SOURCE_RELATIVE_PATH = "home/.gradle/caches/modules-2/files-2.1" | ||
| const val GRADLE_CACHE_RELATIVE_PATH = "home/.gradle/caches" | ||
| const val ARG_DESTINATION_RELATIVE_PATH = "androidide.exportCache.destination" | ||
| const val ARG_GRADLE_API_VERSION = "androidide.exportGradleApi.version" | ||
| const val ARG_GRADLE_API_DESTINATION_RELATIVE_PATH = "androidide.exportGradleApi.destination" | ||
| const val EXPORT_BASE_DIRECTORY = "CodeOnTheGoProjects" | ||
| const val DEFAULT_DESTINATION_RELATIVE_PATH = "CodeOnTheGoProjects/gradle-cache/modules-2/files-2.1" | ||
| const val DEFAULT_GRADLE_API_DESTINATION_RELATIVE_PATH = "CodeOnTheGoProjects/gradle-api" | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.