diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/content/ApplicationKotlinFileBuilder.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/content/ApplicationKotlinFileBuilder.kt index ff7b7b4..6d9e71e 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/content/ApplicationKotlinFileBuilder.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/content/ApplicationKotlinFileBuilder.kt @@ -1,17 +1,16 @@ package com.mitteloupe.cag.core.content -fun buildApplicationKotlinFile(projectNamespace: String): String { - val appName = projectNamespace.split('.').last().capitalized +fun buildApplicationKotlinFile( + projectNamespace: String, + appName: String +): String = + """ + package $projectNamespace - return """ - package $projectNamespace + import android.app.Application + import dagger.hilt.android.HiltAndroidApp - import android.app.Application + @HiltAndroidApp + class ${appName}Application : Application() - class ${appName}Application : Application() { - override fun onCreate() { - super.onCreate() - } - } - """.trimIndent() -} + """.trimIndent() diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/content/MainActivityKotlinFileBuilder.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/content/MainActivityKotlinFileBuilder.kt index dafc9cf..e539e05 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/content/MainActivityKotlinFileBuilder.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/content/MainActivityKotlinFileBuilder.kt @@ -10,8 +10,7 @@ fun buildMainActivityKotlinFile( val result = if (enableCompose) { val optimizedImports = - """ -import android.os.Bundle + """import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize @@ -23,64 +22,60 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import $projectNamespace.ui.theme.${appName}Theme """.optimizeImports() - """ - package $projectNamespace + $$"""package $$projectNamespace - $optimizedImports - class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - ${appName}Theme { - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Greeting("Android") - } - } - } +$$optimizedImports +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + $${appName}Theme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Greeting("Android") } } + } + } +} - @Composable - fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello ${'$'}name!", - modifier = modifier - ) - } +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + Text( + text = "Hello $name!", + modifier = modifier + ) +} - @Preview(showBackground = true) - @Composable - fun GreetingPreview() { - ${appName}Theme { - Greeting("Android") - } - } - """.trimIndent() +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + $${appName}Theme { + Greeting("Android") + } +} +""" } else { val optimizedImports = - """ -import android.os.Bundle + """import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import $projectNamespace.databinding.ActivityMainBinding """.optimizeImports() - """ - package $projectNamespace - - $optimizedImports + """package $projectNamespace - class MainActivity : AppCompatActivity() { - private lateinit var binding: ActivityMainBinding +$optimizedImports +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - } - } - """.trimIndent() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + } +} +""" } return result diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/content/SettingsGradleScriptBuilder.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/content/SettingsGradleScriptBuilder.kt index 23977cb..0cd666c 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/content/SettingsGradleScriptBuilder.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/content/SettingsGradleScriptBuilder.kt @@ -8,13 +8,13 @@ fun buildSettingsGradleScript( featureNames.joinToString( separator = "\n " ) { featureName -> - """ + $$""" setOf("ui", "presentation", "domain", "data").forEach { layer -> - include("features:${featureName.lowercase()}:${'$'}layer") + include("features:$${featureName.lowercase()}:$layer") } """ } - return """ + return $$""" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { @@ -33,7 +33,7 @@ fun buildSettingsGradleScript( } } - rootProject.name = "$projectName" + rootProject.name = "$$projectName" include(":app") include(":coroutine") @@ -44,14 +44,14 @@ fun buildSettingsGradleScript( "presentation-test", "domain" ).forEach { module -> - include(":architecture:${'$'}module") + include(":architecture:$module") } - $featuresBlock + $$featuresBlock setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") } """.trimIndent() } diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/content/gradle/GradleFileExtender.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/content/gradle/GradleFileExtender.kt index 76cdb10..ac9267b 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/content/gradle/GradleFileExtender.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/content/gradle/GradleFileExtender.kt @@ -52,9 +52,9 @@ class GradleFileExtender internal constructor() { configRelativePathFromModule: String = "../../detekt.yml" ): String = if (catalog.isPluginAvailable(PluginConstants.DETEKT)) { - """ + $$""" detekt { - config.setFrom("${'$'}projectDir/$configRelativePathFromModule") + config.setFrom("$projectDir/$$configRelativePathFromModule") } """ } else { diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGenerator.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGenerator.kt index 64b9832..be55cc0 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGenerator.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGenerator.kt @@ -60,12 +60,12 @@ class AppModuleContentGenerator( ) { val appModuleDirectory = File(startDirectory, "app") val sourceRoot = File(appModuleDirectory, "src/main/java") - val basePackageDir = buildPackageDirectory(sourceRoot, projectNamespace.toSegments()) + val basePackageDirectory = buildPackageDirectory(sourceRoot, projectNamespace.toSegments()) - fileCreator.createDirectoryIfNotExists(basePackageDir) + fileCreator.createDirectoryIfNotExists(basePackageDirectory) val sanitizedAppName = appName.withoutSpaces() - val mainActivityFile = File(basePackageDir, "MainActivity.kt") + val mainActivityFile = File(basePackageDirectory, "MainActivity.kt") fileCreator.createFileIfNotExists(mainActivityFile) { buildMainActivityKotlinFile( appName = sanitizedAppName, @@ -74,9 +74,10 @@ class AppModuleContentGenerator( ) } - val applicationFile = File(basePackageDir, "Application.kt") - val applicationContent = buildApplicationKotlinFile(projectNamespace) - fileCreator.createFileIfNotExists(applicationFile) { applicationContent } + val applicationFile = File(basePackageDirectory, "${sanitizedAppName}Application.kt") + fileCreator.createFileIfNotExists(applicationFile) { + buildApplicationKotlinFile(projectNamespace = projectNamespace, appName = sanitizedAppName) + } generateAndroidResources( appModuleDirectory = appModuleDirectory, diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdater.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdater.kt index a2d7a47..a300060 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdater.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdater.kt @@ -75,9 +75,9 @@ class SettingsFileUpdater( throw GenerationException("Failed to read ${settingsFile.name}: ${it.message}") } - val groupedIncludeKts = "include(\"$groupPrefix:${'$'}module\")" - val groupedIncludeGroovyDouble = "include \"$groupPrefix:${'$'}module\"" - val groupedIncludeGroovySingle = "include '$groupPrefix:${'$'}module'" + val groupedIncludeKts = $$"include(\"$$groupPrefix:$module\")" + val groupedIncludeGroovyDouble = $$"include \"$$groupPrefix:$module\"" + val groupedIncludeGroovySingle = $$"include '$$groupPrefix:$module'" val hasGroupedInclude = originalFileContent.contains(groupedIncludeKts) || @@ -134,7 +134,7 @@ class SettingsFileUpdater( "setOf(\n" + modulesKtsBlock + "\n).forEach { module ->\n" + - " include(\"$groupPrefix:${'$'}module\")\n" + + $$" include(\"$$groupPrefix:$module\")\n" + "}" ) } else { @@ -142,7 +142,7 @@ class SettingsFileUpdater( "[\n" + modulesGroovyBlock + "\n].each { module ->\n" + - " include \"$groupPrefix:${'$'}module\"\n" + + $$" include \"$$groupPrefix:$module\"\n" + "}" ) } diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/architecture/InstrumentationTestModuleCreator.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/architecture/InstrumentationTestModuleCreator.kt index a9c1fd1..8340140 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/architecture/InstrumentationTestModuleCreator.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/architecture/InstrumentationTestModuleCreator.kt @@ -208,9 +208,9 @@ import org.hamcrest.Matcher """.optimizeImports() val content = - """package $architecturePackageName.test.assertion + $$"""package $$architecturePackageName.test.assertion -$imports +$$imports fun matchesItemAtPosition(matcher: Matcher?, position: Int) = ViewAssertion { view, noViewFoundException -> if (noViewFoundException != null) { @@ -218,7 +218,7 @@ fun matchesItemAtPosition(matcher: Matcher?, position: Int) = } val recyclerView = view as RecyclerView val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) - ?: throw AssertionFailedError("No view holder at position: ${'$'}position") + ?: throw AssertionFailedError("No view holder at position: $position") assertThat(viewHolder.itemView, matcher) } """ @@ -277,9 +277,9 @@ import androidx.test.uiautomator.UiSelector """.optimizeImports() val content = - """package $architecturePackageName.test.idlingresource + $$"""package $$architecturePackageName.test.idlingresource -$imports +$$imports private const val APP_NOT_RESPONDING_TEXT = " isn't responding" private const val APP_NOT_RESPONDING_TAG = "AppNotResponding" fun UiDevice.registerAppNotRespondingWatcher() { @@ -316,7 +316,7 @@ private fun UiDevice.closeAnrWithWait(appNotRespondingDialog: UiObject) { ).click() val dialogText = appNotRespondingDialog.text val appName = dialogText.take(dialogText.length - APP_NOT_RESPONDING_TEXT.length) - Log.i(APP_NOT_RESPONDING_TAG, "App \"${'$'}appName\" is not responding. Pressed on wait.") + Log.i(APP_NOT_RESPONDING_TAG, "App \"$appName\" is not responding. Pressed on wait.") } catch (uiObjectNotFoundException: UiObjectNotFoundException) { Log.i(APP_NOT_RESPONDING_TAG, "Detected app not responding dialog, but window disappeared.") } @@ -427,14 +427,14 @@ annotation class LocalStore(val localStoreDataIds: Array) packageDirectory = packageDirectory, relativePath = "test/DoesNot.kt", content = - """package $architecturePackageName.test.test + $$"""package $$architecturePackageName.test.test import junit.framework.AssertionFailedError fun doesNot(description: String, block: () -> Unit) { try { block() - error("Unexpected: ${'$'}description") + error("Unexpected: $description") } catch (_: AssertionFailedError) { } } @@ -676,9 +676,9 @@ import org.hamcrest.TypeSafeMatcher """.optimizeImports() val content = - """package $architecturePackageName.test.matcher + $$"""package $$architecturePackageName.test.matcher -$imports +$$imports fun withBackgroundColorMatcher( @ColorInt color: Int, matchCardViewBackgrounds: Boolean @@ -690,7 +690,7 @@ class WithBackgroundColorMatcher( ) : TypeSafeMatcher() { override fun describeTo(description: Description?) { @OptIn(ExperimentalStdlibApi::class) - description?.appendText("has background color: #${'$'}{expectedColor.toHexString()}") + description?.appendText("has background color: #${expectedColor.toHexString()}") } override fun matchesSafely(item: View): Boolean { @@ -698,7 +698,7 @@ class WithBackgroundColorMatcher( (item as? CardView)?.cardBackgroundColor?.getColorForState(item.drawableState, -1) .also { @OptIn(ExperimentalStdlibApi::class) - println("Background color: #${'$'}{it?.toHexString()}") + println("Background color: #${it?.toHexString()}") } } else { (item.background as? ColorDrawable)?.color @@ -746,9 +746,9 @@ import org.hamcrest.TypeSafeMatcher """.optimizeImports() val content = - """package $architecturePackageName.test.matcher + $$"""package $$architecturePackageName.test.matcher -$imports +$$imports fun withDrawableId(@DrawableRes id: Int): Matcher = WithDrawableIdMatcher(id) class WithDrawableIdMatcher(@param:DrawableRes private val expectedId: Int) : @@ -806,10 +806,10 @@ class WithDrawableIdMatcher(@param:DrawableRes private val expectedId: Int) : } override fun describeTo(description: Description) { - description.appendText("with drawable from resource id: ${'$'}expectedId") + description.appendText("with drawable from resource id: $expectedId") val targetContext = InstrumentationRegistry.getInstrumentation().targetContext targetContext.resources.getResourceEntryName(expectedId) - ?.let { description.appendText("[${'$'}it]") } + ?.let { description.appendText("[$it]") } } private fun Drawable.extractStateIfStateful(currentState: IntArray): Drawable? { @@ -820,7 +820,7 @@ class WithDrawableIdMatcher(@param:DrawableRes private val expectedId: Int) : stateListDrawable.findStateDrawableIndex(currentState) ) } else { - Log.w("DrawableMatcher", "Android version ${'$'}{Build.VERSION.SDK_INT} unsupported.") + Log.w("DrawableMatcher", "Android version ${Build.VERSION.SDK_INT} unsupported.") null } } @@ -894,9 +894,9 @@ import org.junit.runners.model.Statement """.optimizeImports() val content = - """package $architecturePackageName.test.rule + $$"""package $$architecturePackageName.test.rule -$imports +$$imports class LocalStoreRule( private val lazySharedPreferences: Lazy, private val lazyKeyValueStore: Lazy @@ -925,7 +925,7 @@ class LocalStoreRule( description.localStoreDataIds() .map { localStoreDataId -> requireNotNull(keyValueStore.keyValues[localStoreDataId]) { - "Request/Response ID ${'$'}localStoreDataId not found." + "Request/Response ID $localStoreDataId not found." } }.forEach { keyValuePair -> val (key, value) = keyValuePair @@ -954,7 +954,7 @@ class LocalStoreRule( putStringSet(key, value as Set) } - else -> throw IllegalArgumentException("${'$'}value is of an unsupported type.") + else -> throw IllegalArgumentException("$value is of an unsupported type.") } } } @@ -995,9 +995,9 @@ import org.junit.runner.Description """.optimizeImports() val content = - """package $architecturePackageName.test.rule + $$"""package $$architecturePackageName.test.rule -$imports +$$imports private const val TAG = "Test" private val deviceLanguage by lazy { Locale.getDefault().language } @@ -1016,7 +1016,7 @@ private val contentValues = ContentValues().apply { class ScreenshotFailureRule : TestWatcher() { override fun failed(e: Throwable?, description: Description) { - val screenShotName = "${'$'}deviceLanguage-${'$'}{description.methodName}-${'$'}{getDate()}" + val screenShotName = "$deviceLanguage-${description.methodName}-${getDate()}" val bitmap = getInstrumentation().uiAutomation.takeScreenshot() storeFailureScreenshot(bitmap, screenShotName) } @@ -1054,7 +1054,7 @@ private fun useMediaStoreScreenshotStorage( contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) ?.let { uri -> - Log.d(TAG, "Saving screenshot to ${'$'}uri") + Log.d(TAG, "Saving screenshot to $uri") contentResolver.openOutputStream(uri)?.let { saveScreenshotToStream(bitmap, it) } contentResolver.update(uri, contentValues, null, null) } @@ -1076,7 +1076,7 @@ private fun usePublicExternalScreenshotStorage( } val file = File(directory, screenshotName.jpg) - Log.d(TAG, "Saving screenshot to ${'$'}{file.absolutePath}") + Log.d(TAG, "Saving screenshot to ${file.absolutePath}") saveScreenshotToStream(bitmap, FileOutputStream(file)) contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) @@ -1088,13 +1088,13 @@ private fun saveScreenshotToStream(bitmap: Bitmap, outputStream: OutputStream) { bitmap.compress(Bitmap.CompressFormat.JPEG, 80, openStream) Log.d(TAG, "Screenshot saved.") } catch (ioException: IOException) { - Log.e(TAG, "Screenshot was not stored at this time: ${'$'}{ioException.message}") + Log.e(TAG, "Screenshot was not stored at this time: ${ioException.message}") } } } private val String.jpg - get() = "${'$'}{this.replace(":", "_")}.jpg" + get() = "${this.replace(":", "_")}.jpg" """ generateFileIfMissing( @@ -1201,9 +1201,9 @@ import org.junit.runners.model.Statement """.optimizeImports() val content = - """package $architecturePackageName.test.rule + $$"""package $$architecturePackageName.test.rule -$imports +$$imports class WebServerRule( private val lazyMockDispatcher: Lazy, private val lazyResponseStore: Lazy @@ -1234,7 +1234,7 @@ private class WebServerStatement( .map { requestResponseId -> requireNotNull( responseStore.responseFactories[requestResponseId] - ) { "Request/Response ID ${'$'}requestResponseId not found." } + ) { "Request/Response ID $requestResponseId not found." } } requestResponses.forEach { requestResponse -> @@ -1251,7 +1251,7 @@ private class WebServerStatement( val unusedResponseKeys = stubbedResponseKeys - usedResponseKeys check(unusedResponseKeys.isEmpty()) { - "${'$'}{unusedResponseKeys.size} unused stubbed URLs:\n[" + + "${unusedResponseKeys.size} unused stubbed URLs:\n[" + unusedResponseKeys.joinToString("]\n[") + "]" } } @@ -1284,9 +1284,9 @@ import javax.net.ssl.SSLSocketFactory """.optimizeImports() val content = - """package $architecturePackageName.test.server + $$"""package $$architecturePackageName.test.server -$imports +$$imports class LoggingSslSocketFactory(private val delegate: SSLSocketFactory) : SSLSocketFactory() { override fun getDefaultCipherSuites(): Array = delegate.defaultCipherSuites @@ -1322,7 +1322,7 @@ class LoggingSslSocketFactory(private val delegate: SSLSocketFactory) : SSLSocke socket.addHandshakeCompletedListener { event -> Log.d( "SSL", - "Handshake completed with peerHost=${'$'}{event.socket.inetAddress.hostName}" + "Handshake completed with peerHost=${event.socket.inetAddress.hostName}" ) } } @@ -1356,9 +1356,9 @@ import okhttp3.mockwebserver.RecordedRequest """.optimizeImports() val content = - """package $architecturePackageName.test.server + $$"""package $$architecturePackageName.test.server -$imports +$$imports class MockDispatcher : Dispatcher(), ResponseBinder { @@ -1390,7 +1390,7 @@ class MockDispatcher : usedEndpoints.add(requestResponse.key) } val response = matchingRequest?.value?.mockResponse() ?: MockResponse(code = 404).also { - Log.w(TAG, "${'$'}testName: ${'$'}{request.path} not stubbed!") + Log.w(TAG, "$testName: ${request.path} not stubbed!") } return if (response.upgradeToWebSocket) { MockResponse().withWebSocketUpgrade( diff --git a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/architecture/UiModuleCreator.kt b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/architecture/UiModuleCreator.kt index df7ee5c..91894d7 100644 --- a/core/src/main/kotlin/com/mitteloupe/cag/core/generation/architecture/UiModuleCreator.kt +++ b/core/src/main/kotlin/com/mitteloupe/cag/core/generation/architecture/UiModuleCreator.kt @@ -54,13 +54,13 @@ interface ViewStateBinder - include(":architecture:${'$'}module") + include(":architecture:$module") } setOf("ui", "presentation", "domain", "data").forEach { layer -> - include("features:samplefeature:${'$'}layer") + include("features:samplefeature:$layer") } setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") }""" // When @@ -64,7 +64,7 @@ setOf( // Given val projectName = "TestProject" val featureNames = listOf("SampleFeature") - val expectedContent = """enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + val expectedContent = $$"""enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { @@ -93,18 +93,18 @@ setOf( "presentation-test", "domain" ).forEach { module -> - include(":architecture:${'$'}module") + include(":architecture:$module") } setOf("ui", "presentation", "domain", "data").forEach { layer -> - include("features:samplefeature:${'$'}layer") + include("features:samplefeature:$layer") } setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") }""" // When @@ -119,7 +119,7 @@ setOf( // Given val projectName = "TestProject" val featureNames = listOf("SampleFeature") - val expectedContent = """enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + val expectedContent = $$"""enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { @@ -148,18 +148,18 @@ setOf( "presentation-test", "domain" ).forEach { module -> - include(":architecture:${'$'}module") + include(":architecture:$module") } setOf("ui", "presentation", "domain", "data").forEach { layer -> - include("features:samplefeature:${'$'}layer") + include("features:samplefeature:$layer") } setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") }""" // When diff --git a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGeneratorTest.kt b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGeneratorTest.kt new file mode 100644 index 0000000..a4b0fd4 --- /dev/null +++ b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/AppModuleContentGeneratorTest.kt @@ -0,0 +1,151 @@ +package com.mitteloupe.cag.core.generation + +import com.mitteloupe.cag.core.DirectoryFinder +import com.mitteloupe.cag.core.fake.FakeFileSystemBridge +import com.mitteloupe.cag.core.generation.filesystem.FileCreator +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.io.File +import kotlin.io.path.createTempDirectory + +class AppModuleContentGeneratorTest { + private lateinit var classUnderTest: AppModuleContentGenerator + + @Before + fun setUp() { + val fileCreator = FileCreator(FakeFileSystemBridge()) + val directoryFinder = DirectoryFinder() + classUnderTest = AppModuleContentGenerator(fileCreator, directoryFinder) + } + + @Test + fun `Given no compose when writeAppModule then generates MainActivity file`() { + val startDirectory = createTempDirectory(prefix = "appModule").toFile() + val appName = "TestApp" + val namespace = "com.company.testapp" + val enableCompose = false + val expectedContent = """package com.company.testapp + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.company.testapp.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + } +} +""" + + // When + classUnderTest.writeAppModule( + startDirectory = startDirectory, + appName = appName, + projectNamespace = namespace, + enableCompose = enableCompose + ) + val mainActivityFile = File(startDirectory, "app/src/main/java/com/company/testapp/MainActivity.kt") + + // Then + assertEquals(expectedContent, mainActivityFile.readText()) + } + + @Test + fun `Given compose when writeAppModule then generates MainActivity file`() { + val startDirectory = createTempDirectory(prefix = "appModule").toFile() + val appName = "TestApp" + val namespace = "com.company.testapp" + val enableCompose = true + val expectedContent = $$"""package $$namespace + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.company.testapp.ui.theme.TestAppTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + TestAppTheme { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background + ) { + Greeting("Android") + } + } + } + } +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + Text( + text = "Hello $name!", + modifier = modifier + ) +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + TestAppTheme { + Greeting("Android") + } +} +""" + + // When + classUnderTest.writeAppModule( + startDirectory = startDirectory, + appName = appName, + projectNamespace = namespace, + enableCompose = enableCompose + ) + val mainActivityFile = File(startDirectory, "app/src/main/java/com/company/testapp/MainActivity.kt") + + // Then + assertEquals(expectedContent, mainActivityFile.readText()) + } + + @Test + fun `Given no compose when writeAppModule then generates HappinessTrackerApplication file`() { + val startDirectory = createTempDirectory(prefix = "appModule").toFile() + val appName = "HappinessTracker" + val namespace = "com.happycorp.happinesstracker" + val enableCompose = false + val expectedContent = """package $namespace + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class HappinessTrackerApplication : Application() +""" + + // When + classUnderTest.writeAppModule( + startDirectory = startDirectory, + appName = appName, + projectNamespace = namespace, + enableCompose = enableCompose + ) + val mainActivityFile = File(startDirectory, "app/src/main/java/com/happycorp/happinesstracker/HappinessTrackerApplication.kt") + + // Then + assertEquals(expectedContent, mainActivityFile.readText()) + } +} diff --git a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/BuildSrcContentCreatorTest.kt b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/BuildSrcContentCreatorTest.kt index 00307a3..a506a32 100644 --- a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/BuildSrcContentCreatorTest.kt +++ b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/BuildSrcContentCreatorTest.kt @@ -22,12 +22,6 @@ class BuildSrcContentCreatorTest { val projectRoot = createTempDirectory(prefix = "projectRoot").toFile() val buildSrcDir = File(projectRoot, "buildSrc") buildSrcDir.mkdirs() - - // When - classUnderTest.writeGradleFile(projectRoot) - - // Then - val buildSrcGradleFile = File(buildSrcDir, "build.gradle.kts") val expectedContent = """plugins { `kotlin-dsl` } @@ -36,6 +30,12 @@ repositories { mavenCentral() } """ + + // When + classUnderTest.writeGradleFile(projectRoot) + val buildSrcGradleFile = File(buildSrcDir, "build.gradle.kts") + + // Then assertEquals(expectedContent, buildSrcGradleFile.readText()) } @@ -62,12 +62,6 @@ repositories { val projectRoot = createTempDirectory(prefix = "projectRoot3").toFile() val buildSrcDir = File(projectRoot, "buildSrc") buildSrcDir.mkdirs() - - // When - classUnderTest.writeSettingsGradleFile(projectRoot) - - // Then - val buildSrcSettingsFile = File(buildSrcDir, "settings.gradle.kts") val expectedContent = """rootProject.name = "buildSrc" pluginManagement { @@ -78,6 +72,12 @@ pluginManagement { } } """ + + // When + classUnderTest.writeSettingsGradleFile(projectRoot) + val buildSrcSettingsFile = File(buildSrcDir, "settings.gradle.kts") + + // Then assertEquals(expectedContent, buildSrcSettingsFile.readText()) } diff --git a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdaterTest.kt b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdaterTest.kt index ec212e5..798444e 100644 --- a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdaterTest.kt +++ b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/SettingsFileUpdaterTest.kt @@ -38,14 +38,14 @@ class SettingsFileUpdaterTest { ) val givenFeatureNameLowerCase = "feature" val expectedTail = - """ + $$""" setOf( "ui", "presentation", "domain", "data" ).forEach { module -> - include(":features:$givenFeatureNameLowerCase:${'$'}module") + include(":features:$$givenFeatureNameLowerCase:$module") } """.trimIndent() @@ -69,14 +69,14 @@ class SettingsFileUpdaterTest { ) val givenFeatureNameLowerCase = "feat" val expectedTail = - """ + $$""" setOf( "ui", "presentation", "domain", "data" ).forEach { module -> - include(":features:$givenFeatureNameLowerCase:${'$'}module") + include(":features:$$givenFeatureNameLowerCase:$module") } """.trimIndent() val expectedNewlineBeforeIncludes = "rootProject.name = \"app\"\n$expectedTail" @@ -97,7 +97,7 @@ class SettingsFileUpdaterTest { // Given val givenFeatureNameLowerCase = "ready" val initial = - """ + $$""" rootProject.name = "app" setOf( "ui", @@ -105,7 +105,7 @@ class SettingsFileUpdaterTest { "domain", "data" ).forEach { module -> - include(":features:$givenFeatureNameLowerCase:${'$'}module") + include(":features:$$givenFeatureNameLowerCase:$module") } """.trimIndent() @@ -131,14 +131,14 @@ class SettingsFileUpdaterTest { ) val givenFeatureNameLowerCase = "testfeature" val expectedTail = - """ + $$""" [ 'ui', 'presentation', 'domain', 'data' ].each { module -> - include ":features:$givenFeatureNameLowerCase:${'$'}module" + include ":features:$$givenFeatureNameLowerCase:$module" } """.trimIndent() @@ -167,14 +167,14 @@ class SettingsFileUpdaterTest { val (projectRoot, startDirectory) = createProjectWithKotlinSettings(initialContent = initial) val expectedTail = - """ + $$""" setOf( "ui", "presentation", "domain", "data" ).forEach { module -> - include(":features:$givenFeatureNameLowerCase:${'$'}module") + include(":features:$$givenFeatureNameLowerCase:$module") } """.trimIndent() @@ -202,14 +202,14 @@ class SettingsFileUpdaterTest { val (projectRoot, startDirectory) = createProjectWithGroovySettings(initialContent = initial) val expectedTail = - """ + $$""" [ 'ui', 'presentation', 'domain', 'data' ].each { module -> - include ":features:$givenFeatureNameLowerCase:${'$'}module" + include ":features:$$givenFeatureNameLowerCase:$module" } """.trimIndent() @@ -229,7 +229,7 @@ class SettingsFileUpdaterTest { // Given val givenFeatureNameLowerCase = "feat" val initial = - """ + $$""" rootProject.name = "app" setOf( "ui", @@ -237,7 +237,7 @@ class SettingsFileUpdaterTest { "domain", "data" ).forEach { module -> - include(":features:$givenFeatureNameLowerCase:${'$'}module") + include(":features:$$givenFeatureNameLowerCase:$module") } """.trimIndent() + "\n" val (projectRoot, startDirectory) = createProjectWithKotlinSettings(initialContent = initial) @@ -258,7 +258,7 @@ class SettingsFileUpdaterTest { // Given val givenFeatureNameLowerCase = "feat" val initial = - """ + $$""" rootProject.name = 'app' [ 'ui', @@ -266,7 +266,7 @@ class SettingsFileUpdaterTest { 'domain', 'data' ].each { module -> - include ":features:$givenFeatureNameLowerCase:${'$'}module" + include ":features:$$givenFeatureNameLowerCase:$module" } """.trimIndent() + "\n" val (projectRoot, startDirectory) = createProjectWithGroovySettings(initialContent = initial) @@ -323,12 +323,12 @@ class SettingsFileUpdaterTest { initialContent = "rootProject.name = \"app\"\n" ) val expectedTail = - """ + $$""" setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") } """.trimIndent() @@ -348,12 +348,12 @@ class SettingsFileUpdaterTest { initialContent = "rootProject.name = \"app\"" ) val expectedTail = - """ + $$""" setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") } """.trimIndent() val expectedNewlineBeforeIncludes = "rootProject.name = \"app\"\n$expectedTail" @@ -379,12 +379,12 @@ class SettingsFileUpdaterTest { val (projectRoot, startDirectory) = createProjectWithKotlinSettings(initialContent = initial) val expectedTail = - """ + $$""" setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") } """.trimIndent() @@ -408,12 +408,12 @@ class SettingsFileUpdaterTest { val (projectRoot, startDirectory) = createProjectWithKotlinSettings(initialContent = initial) val expectedTail = - """ + $$""" setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") } """.trimIndent() @@ -433,12 +433,12 @@ class SettingsFileUpdaterTest { initialContent = "rootProject.name = 'app'\n" ) val expectedTail = - """ + $$""" [ 'source', 'implementation' ].each { module -> - include ":datasource:${'$'}module" + include ":datasource:$module" } """.trimIndent() @@ -461,12 +461,12 @@ class SettingsFileUpdaterTest { val (projectRoot, startDirectory) = createProjectWithGroovySettings(initialContent = initial) val expectedTail = - """ + $$""" [ 'source', 'implementation' ].each { module -> - include ":datasource:${'$'}module" + include ":datasource:$module" } """.trimIndent() @@ -482,13 +482,13 @@ class SettingsFileUpdaterTest { fun `Given grouped includes already present in Kotlin when updateDataSourceSettingsIfPresent then does nothing`() { // Given val initial = - """ + $$""" rootProject.name = "app" setOf( "source", "implementation" ).forEach { module -> - include(":datasource:${'$'}module") + include(":datasource:$module") } """.trimIndent() + "\n" val (projectRoot, startDirectory) = createProjectWithKotlinSettings(initialContent = initial) @@ -505,13 +505,13 @@ class SettingsFileUpdaterTest { fun `Given grouped includes already present in Groovy when updateDataSourceSettingsIfPresent then does nothing`() { // Given val initial = - """ + $$""" rootProject.name = 'app' [ 'source', 'implementation' ].each { module -> - include ":datasource:${'$'}module" + include ":datasource:$module" } """.trimIndent() + "\n" val (projectRoot, startDirectory) = createProjectWithGroovySettings(initialContent = initial) @@ -534,7 +534,7 @@ class SettingsFileUpdaterTest { initialContent = "rootProject.name = \"app\"\n" ) val expectedTail = - """ + $$""" setOf( "ui", "instrumentation-test", @@ -542,7 +542,7 @@ class SettingsFileUpdaterTest { "presentation-test", "domain" ).forEach { module -> - include(":architecture:${'$'}module") + include(":architecture:$module") } """.trimIndent() @@ -562,7 +562,7 @@ class SettingsFileUpdaterTest { initialContent = "rootProject.name = 'app'\n" ) val expectedTail = - """ + $$""" [ 'ui', 'instrumentation-test', @@ -570,7 +570,7 @@ class SettingsFileUpdaterTest { 'presentation-test', 'domain' ].each { module -> - include ":architecture:${'$'}module" + include ":architecture:$module" } """.trimIndent() diff --git a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/architecture/ArchitectureModulesContentGeneratorTest.kt b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/architecture/ArchitectureModulesContentGeneratorTest.kt index 4ea453f..aae4cc6 100644 --- a/core/src/test/kotlin/com/mitteloupe/cag/core/generation/architecture/ArchitectureModulesContentGeneratorTest.kt +++ b/core/src/test/kotlin/com/mitteloupe/cag/core/generation/architecture/ArchitectureModulesContentGeneratorTest.kt @@ -947,13 +947,13 @@ interface ViewStateBinder) // Then val itemAtPositionMatcherFile = File(instrumentationTestRoot, "src/main/java/com/example/architecture/test/assertion/ItemAtPositionMatcher.kt") - val expectedContent = """package com.example.architecture.test.assertion + val expectedContent = $$"""package com.example.architecture.test.assertion import android.view.View import androidx.recyclerview.widget.RecyclerView @@ -1767,7 +1767,7 @@ fun matchesItemAtPosition(matcher: Matcher?, position: Int) = } val recyclerView = view as RecyclerView val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) - ?: throw AssertionFailedError("No view holder at position: ${'$'}position") + ?: throw AssertionFailedError("No view holder at position: $position") assertThat(viewHolder.itemView, matcher) } """ @@ -1818,7 +1818,7 @@ fun processAssetStream( // Then val appNotRespondingHandlerFile = File(instrumentationTestRoot, "src/main/java/com/example/architecture/test/idlingresource/AppNotRespondingHandler.kt") - val expectedContent = """package com.example.architecture.test.idlingresource + val expectedContent = $$"""package com.example.architecture.test.idlingresource import android.util.Log import androidx.test.uiautomator.UiDevice @@ -1862,7 +1862,7 @@ private fun UiDevice.closeAnrWithWait(appNotRespondingDialog: UiObject) { ).click() val dialogText = appNotRespondingDialog.text val appName = dialogText.take(dialogText.length - APP_NOT_RESPONDING_TEXT.length) - Log.i(APP_NOT_RESPONDING_TAG, "App \"${'$'}appName\" is not responding. Pressed on wait.") + Log.i(APP_NOT_RESPONDING_TAG, "App \"$appName\" is not responding. Pressed on wait.") } catch (uiObjectNotFoundException: UiObjectNotFoundException) { Log.i(APP_NOT_RESPONDING_TAG, "Detected app not responding dialog, but window disappeared.") } @@ -1884,14 +1884,14 @@ private fun UiDevice.closeAnrWithWait(appNotRespondingDialog: UiObject) { // Then val doesNotFile = File(instrumentationTestRoot, "src/main/java/com/example/architecture/test/test/DoesNot.kt") - val expectedContent = """package com.example.architecture.test.test + val expectedContent = $$"""package com.example.architecture.test.test import junit.framework.AssertionFailedError fun doesNot(description: String, block: () -> Unit) { try { block() - error("Unexpected: ${'$'}description") + error("Unexpected: $description") } catch (_: AssertionFailedError) { } } @@ -2136,7 +2136,7 @@ abstract class KeyValueStore { // Then val withBackgroundColorMatcherFile = File(instrumentationTestRoot, "src/main/java/com/example/architecture/test/matcher/WithBackgroundColorMatcher.kt") - val expectedContent = """package com.example.architecture.test.matcher + val expectedContent = $$"""package com.example.architecture.test.matcher import android.graphics.drawable.ColorDrawable import android.view.View @@ -2157,7 +2157,7 @@ class WithBackgroundColorMatcher( ) : TypeSafeMatcher() { override fun describeTo(description: Description?) { @OptIn(ExperimentalStdlibApi::class) - description?.appendText("has background color: #${'$'}{expectedColor.toHexString()}") + description?.appendText("has background color: #${expectedColor.toHexString()}") } override fun matchesSafely(item: View): Boolean { @@ -2165,7 +2165,7 @@ class WithBackgroundColorMatcher( (item as? CardView)?.cardBackgroundColor?.getColorForState(item.drawableState, -1) .also { @OptIn(ExperimentalStdlibApi::class) - println("Background color: #${'$'}{it?.toHexString()}") + println("Background color: #${it?.toHexString()}") } } else { (item.background as? ColorDrawable)?.color @@ -2196,7 +2196,7 @@ class WithBackgroundColorMatcher( // Then val withDrawableIdMatcherFile = File(instrumentationTestRoot, "src/main/java/com/example/architecture/test/matcher/WithDrawableIdMatcher.kt") - val expectedContent = """package com.example.architecture.test.matcher + val expectedContent = $$"""package com.example.architecture.test.matcher import android.graphics.Bitmap import android.graphics.Canvas @@ -2276,10 +2276,10 @@ class WithDrawableIdMatcher(@param:DrawableRes private val expectedId: Int) : } override fun describeTo(description: Description) { - description.appendText("with drawable from resource id: ${'$'}expectedId") + description.appendText("with drawable from resource id: $expectedId") val targetContext = InstrumentationRegistry.getInstrumentation().targetContext targetContext.resources.getResourceEntryName(expectedId) - ?.let { description.appendText("[${'$'}it]") } + ?.let { description.appendText("[$it]") } } private fun Drawable.extractStateIfStateful(currentState: IntArray): Drawable? { @@ -2290,7 +2290,7 @@ class WithDrawableIdMatcher(@param:DrawableRes private val expectedId: Int) : stateListDrawable.findStateDrawableIndex(currentState) ) } else { - Log.w("DrawableMatcher", "Android version ${'$'}{Build.VERSION.SDK_INT} unsupported.") + Log.w("DrawableMatcher", "Android version ${Build.VERSION.SDK_INT} unsupported.") null } } @@ -2638,7 +2638,7 @@ abstract class ResponseStore { // Then val buildGradleFile = File(architectureRoot, "domain/build.gradle.kts") - val expectedContent = """plugins { + val expectedContent = $$"""plugins { id("project-java-library") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.ktlint) @@ -2651,7 +2651,7 @@ ktlint { } detekt { - config.setFrom("${'$'}projectDir/../../detekt.yml") + config.setFrom("$projectDir/../../detekt.yml") } dependencies { @@ -2703,7 +2703,7 @@ dependencies { // Then val buildGradleFile = File(architectureRoot, "presentation/build.gradle.kts") - val expectedContent = """plugins { + val expectedContent = $$"""plugins { id("project-java-library") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.ktlint) @@ -2722,7 +2722,7 @@ ktlint { } detekt { - config.setFrom("${'$'}projectDir/../../detekt.yml") + config.setFrom("$projectDir/../../detekt.yml") } dependencies { @@ -2748,7 +2748,7 @@ dependencies { // Then val buildGradleFile = File(architectureRoot, "ui/build.gradle.kts") - val expectedContent = """plugins { + val expectedContent = $$"""plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) alias(libs.plugins.ksp) @@ -2788,7 +2788,7 @@ ktlint { } detekt { - config.setFrom("${'$'}projectDir/../../detekt.yml") + config.setFrom("$projectDir/../../detekt.yml") } dependencies { @@ -2820,7 +2820,7 @@ dependencies { // Then val buildGradleFile = File(architectureRoot, "presentation-test/build.gradle.kts") - val expectedContent = """plugins { + val expectedContent = $$"""plugins { id("project-java-library") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.ktlint) @@ -2833,7 +2833,7 @@ ktlint { } detekt { - config.setFrom("${'$'}projectDir/../../detekt.yml") + config.setFrom("$projectDir/../../detekt.yml") } dependencies { @@ -2866,7 +2866,7 @@ dependencies { // Then val buildGradleFile = File(architectureRoot, "instrumentation-test/build.gradle.kts") - val expectedContent = """plugins { + val expectedContent = $$"""plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) alias(libs.plugins.ktlint) @@ -2911,7 +2911,7 @@ ktlint { } detekt { - config.setFrom("${'$'}projectDir/../../detekt.yml") + config.setFrom("$projectDir/../../detekt.yml") } dependencies { @@ -2988,14 +2988,14 @@ dependencies { // Then val buildGradleFile = File(architectureRoot, "domain/build.gradle.kts") - val expectedContent = """plugins { + val expectedContent = $$"""plugins { id("project-java-library") alias(libs.plugins.kotlin.jvm) alias(libs.plugins.detekt) } detekt { - config.setFrom("${'$'}projectDir/../../detekt.yml") + config.setFrom("$projectDir/../../detekt.yml") } dependencies {