From 6804d0a0af3ecd95ab6a192581d9609f28afb211 Mon Sep 17 00:00:00 2001 From: "Alexander.Likhachev" Date: Mon, 1 May 2023 13:42:58 +0200 Subject: [PATCH] [Build, IGS] Implement LocalPropertiesModifier #KTI-1223 In Progress --- .../main/kotlin/LocalPropertiesModifier.kt | 84 ++++++ .../kotlin/LocalPropertiesModifierTest.kt | 256 ++++++++++++++++++ 2 files changed, 340 insertions(+) create mode 100644 repo/gradle-settings-conventions/internal-gradle-setup/src/main/kotlin/LocalPropertiesModifier.kt create mode 100644 repo/gradle-settings-conventions/internal-gradle-setup/src/test/kotlin/LocalPropertiesModifierTest.kt diff --git a/repo/gradle-settings-conventions/internal-gradle-setup/src/main/kotlin/LocalPropertiesModifier.kt b/repo/gradle-settings-conventions/internal-gradle-setup/src/main/kotlin/LocalPropertiesModifier.kt new file mode 100644 index 00000000000..2f092cf8e88 --- /dev/null +++ b/repo/gradle-settings-conventions/internal-gradle-setup/src/main/kotlin/LocalPropertiesModifier.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.build + +import java.io.File +import java.io.StringReader +import java.util.Properties + +private const val SYNCED_PROPERTIES_START_LINE = "# Automatically configured by the `internal-gradle-setup` plugin" + +internal val SYNCED_PROPERTIES_START_LINES = """ + $SYNCED_PROPERTIES_START_LINE + # Please do not edit these properties manually, the changes will be lost + # If you want to override some values, put them before this section and remove from this section +""".trimIndent() + +internal const val SYNCED_PROPERTIES_END_LINE = "# the end of automatically configured properties" + +internal class LocalPropertiesModifier(private val localProperties: File) { + private val manuallyConfiguredPropertiesContent by lazy { + if (!localProperties.exists()) return@lazy "" + var insideAutomaticallyConfiguredSection = false + // filter out the automatically configured lines + localProperties.readLines().filter { line -> + if (line == SYNCED_PROPERTIES_START_LINE) { + insideAutomaticallyConfiguredSection = true + } + val shouldIncludeThisLine = !insideAutomaticallyConfiguredSection + if (line == SYNCED_PROPERTIES_END_LINE) { + insideAutomaticallyConfiguredSection = false + } + shouldIncludeThisLine + }.joinToString("\n") + } + + fun applySetup(setupFile: SetupFile) { + localProperties.parentFile.apply { + if (!exists()) { + mkdirs() + } + } + if (localProperties.exists() && !localProperties.isFile) { + error("$localProperties is not a file!") + } + val manuallyConfiguredProperties = Properties().apply { + StringReader(manuallyConfiguredPropertiesContent).use { + load(it) + } + } + val propertiesToSetup = setupFile.properties.mapValues { PropertyValue(it.value, manuallyConfiguredProperties.containsKey(it.key)) } + localProperties.writeText( + """ + |${manuallyConfiguredPropertiesContent.addSuffix("\n")} + |$SYNCED_PROPERTIES_START_LINES + |${propertiesToSetup.asPropertiesLines} + |$SYNCED_PROPERTIES_END_LINE + | + """.trimMargin() + ) + } +} + +private fun String.addSuffix(suffix: String): String { + if (this.endsWith(suffix)) return this + return "$this$suffix" +} + +internal data class PropertyValue( + val value: String, + val isOverridden: Boolean = false, +) + +internal val Map.asPropertiesLines: String + get() = map { (key, value) -> + when (value.isOverridden) { + true -> """ + #$key=${value.value} the property is overridden + """.trimIndent() + false -> "$key=${value.value}" + } + }.joinToString("\n") \ No newline at end of file diff --git a/repo/gradle-settings-conventions/internal-gradle-setup/src/test/kotlin/LocalPropertiesModifierTest.kt b/repo/gradle-settings-conventions/internal-gradle-setup/src/test/kotlin/LocalPropertiesModifierTest.kt new file mode 100644 index 00000000000..ec9bf669d5f --- /dev/null +++ b/repo/gradle-settings-conventions/internal-gradle-setup/src/test/kotlin/LocalPropertiesModifierTest.kt @@ -0,0 +1,256 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.build + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.io.TempDir +import java.io.FileInputStream +import java.nio.file.Files +import java.nio.file.Path +import java.util.* +import kotlin.test.* + +class LocalPropertiesModifierTest { + @TempDir + lateinit var workingDir: Path + + private val localPropertiesFile by lazy { + workingDir.resolve("local.properties") + } + + private val modifier by lazy { + LocalPropertiesModifier(localPropertiesFile.toFile()) + } + + private val setupFile = SetupFile( + mapOf( + "newProperty1" to "someValue", + "newProperty2" to "someOtherValue", + "alreadySetProperty" to "newValue", + ) + ) + + @Test + @DisplayName("sync is able to create local.properties file") + fun testSyncingWithAbsentFile() { + assertTrue(Files.notExists(localPropertiesFile)) + modifier.applySetup(setupFile) + assertTrue(Files.exists(localPropertiesFile)) + + localPropertiesFile.propertiesFileContentAssertions { fileContents, properties -> + assertContainsMarkersOnce(fileContents) + assertEquals(setupFile.properties.size, properties.size) + for ((key, value) in setupFile.properties) { + assertEquals(value, properties[key]) + } + } + } + + @Test + @DisplayName("sync populates empty local.properties file") + fun testSyncingIntoEmptyFile() { + Files.createFile(localPropertiesFile) + modifier.applySetup(setupFile) + assertTrue(Files.exists(localPropertiesFile)) + + localPropertiesFile.propertiesFileContentAssertions { fileContents, properties -> + assertContainsMarkersOnce(fileContents) + assertEquals(setupFile.properties.size, properties.size) + for ((key, value) in setupFile.properties) { + assertEquals(value, properties[key]) + } + } + } + + /** + * Checks that a file like + * ``` + * a=1 + * b=2 + * c=3 + * ``` + * is being transformed into + * ``` + * a=1 + * b=2 + * c=3 + * #header + * d=4 + * f=5 + * #footer + * ``` + */ + @Test + @DisplayName("sync shouldn't remove any existing properties not managed by the sync") + fun testSyncingIntoNonEmptyFile() { + val initialContent = mapOf( + "oldProperty1" to PropertyValue("oldValue1"), + "oldProperty2" to PropertyValue("oldValue2"), + ) + fillInitialLocalPropertiesFile(initialContent) + + modifier.applySetup(setupFile) + + localPropertiesFile.propertiesFileContentAssertions { fileContents, properties -> + assertContainsMarkersOnce(fileContents) + val expectedProperties = setupFile.properties + initialContent.mapValues { it.value.value } + assertEquals(expectedProperties.size, properties.size) + for ((key, value) in expectedProperties) { + assertEquals(value, properties[key]) + } + } + } + + /** + * Checks that a file like + * ``` + * a=1 + * b=2 + * f=3 + * ``` + * is being transformed into + * ``` + * a=1 + * b=2 + * c=3 + * #header + * d=4 + * #footer + * ``` + */ + @Test + @DisplayName("sync shouldn't override properties if they already manually set") + fun testSyncingDoesNotOverrideValues() { + val initialContent = mapOf( + "oldProperty1" to PropertyValue("oldValue1"), + "oldProperty2" to PropertyValue("oldValue2"), + "alreadySetProperty" to PropertyValue("oldValue3"), + ) + fillInitialLocalPropertiesFile(initialContent) + + modifier.applySetup(setupFile) + + localPropertiesFile.propertiesFileContentAssertions { fileContents, properties -> + assertContainsMarkersOnce(fileContents) + val expectedProperties = setupFile.properties + initialContent.mapValues { it.value.value } + assertEquals(expectedProperties.size, properties.size) + for ((key, value) in expectedProperties) { + assertEquals(value, properties[key]) + } + } + } + + /** + * Checks that a file like + * ``` + * a=1 + * b=2 + * c=3 + * #header + * d=4 + * #footer + * e=5 + * ``` + * is being transformed into + * ``` + * a=1 + * b=2 + * c=3 + * e=5 + * #header + * d=10 + * #footer + * ``` + */ + @Test + @DisplayName("sync should override automatically set properties") + fun testSyncingOverrideAutomaticallySetValues() { + val initialContent = mapOf( + "oldProperty1" to PropertyValue("oldValue1"), + "oldProperty2" to PropertyValue("oldValue2"), + "alreadySetProperty" to PropertyValue("oldValue3"), + ) + fillInitialLocalPropertiesFile(initialContent) + + modifier.applySetup(setupFile) + + val newProperties = mapOf( + "newManualProperty" to PropertyValue("5"), + "otherAlreadySetProperty" to PropertyValue("5"), + ) + fillInitialLocalPropertiesFile(newProperties) + + val anotherSetupFile = SetupFile( + mapOf( + "newProperty2" to "other", // a new value + "newProperty3" to "someOtherValue", // a new record + "otherAlreadySetProperty" to "someOtherValue", + ) + ) + + modifier.applySetup(anotherSetupFile) + + localPropertiesFile.propertiesFileContentAssertions { fileContents, properties -> + assertContainsMarkersOnce(fileContents) + val expectedProperties = + anotherSetupFile.properties + initialContent.mapValues { it.value.value } + newProperties.mapValues { it.value.value } + assertEquals(expectedProperties.size, properties.size) + for ((key, value) in expectedProperties) { + assertEquals(value, properties[key]) + } + } + } + + private fun assertContainsMarkersOnce(content: String) { + assertContainsOnce(content, SYNCED_PROPERTIES_START_LINES) + assertContainsOnce(content, SYNCED_PROPERTIES_END_LINE) + } + + private fun assertContainsOnce(content: String, substring: String) { + var currentOffset = 0 + var count = 0 + var nextIndex = content.indexOf(substring, currentOffset) + + while (nextIndex != -1 && count < 2) { + count++ + currentOffset = nextIndex + substring.length + nextIndex = content.indexOf(substring, currentOffset) + } + assertEquals(1, count) + } + + private fun fillInitialLocalPropertiesFile(content: Map) { + val localPropertiesFile = localPropertiesFile.toFile() + localPropertiesFile.appendText( + """ + |${content.asPropertiesLines} + """.trimMargin() + ) + } + + private fun Path.propertiesFileContentAssertions(assertions: (String, Properties) -> Unit) { + val fileContent = Files.readAllLines(this).joinToString("\n") + try { + val localProperties = Properties().apply { + FileInputStream(localPropertiesFile.toFile()).use { + load(it) + } + } + assertions(fileContent, localProperties) + } catch (e: Throwable) { + println( + """ + |Some assertions on the properties file have failed. + |File contents: + |====== + |$fileContent + |====== + """.trimMargin() + ) + throw e + } + } +} \ No newline at end of file