[Build, IGS] Implement ConsentManager

#KTI-1223 In Progress
This commit is contained in:
Alexander.Likhachev
2023-05-03 20:28:41 +02:00
committed by Space Team
parent 6804d0a0af
commit f04f1d18c4
5 changed files with 174 additions and 20 deletions
@@ -0,0 +1,59 @@
/*
* 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.BufferedReader
import java.io.PrintStream
private val isInIdeaSync
get() = System.getProperty("idea.sync.active").toBoolean()
internal const val USER_CONSENT_MARKER = "# This line indicates that you have chosen to enable automatic configuration of properties."
internal const val USER_REFUSAL_MARKER =
"# This line indicates that you have chosen to disable automatic configuration of properties. If you want to enable it, remove this line."
internal val USER_CONSENT_REQUEST = """
! ATTENTION REQUIRED !
Most probably you're a developer from the Kotlin team. We are asking for your consent for automatic configuration of local.properties file
for providing some optimizations and collecting additional debug information.
""".trimIndent()
internal const val PROMPT_REQUEST = "Do you agree with this? Please answer with 'yes' or 'no': "
internal class ConsentManager(
private val modifier: LocalPropertiesModifier,
private val input: BufferedReader = System.`in`.bufferedReader(),
private val output: PrintStream = System.out,
) {
fun getUserDecision() = when {
modifier.initiallyContains(USER_REFUSAL_MARKER) -> false
modifier.initiallyContains(USER_CONSENT_MARKER) -> true
isInIdeaSync -> false
else -> null
}
fun askForConsent(): Boolean {
output.println(USER_CONSENT_REQUEST)
while (true) {
output.println(PROMPT_REQUEST)
when (input.readLine()) {
"yes" -> {
output.println("You've given the consent")
modifier.putLine(USER_CONSENT_MARKER)
return true
}
"no" -> {
output.println("You've refused to give the consent")
modifier.putLine(USER_REFUSAL_MARKER)
return false
}
}
}
}
}
@@ -20,11 +20,13 @@ internal val SYNCED_PROPERTIES_START_LINES = """
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 ""
private val initialUserConfiguredPropertiesContent = getUserConfiguredPropertiesContent()
private fun getUserConfiguredPropertiesContent(): String {
if (!localProperties.exists()) return ""
var insideAutomaticallyConfiguredSection = false
// filter out the automatically configured lines
localProperties.readLines().filter { line ->
return localProperties.readLines().filter { line ->
if (line == SYNCED_PROPERTIES_START_LINE) {
insideAutomaticallyConfiguredSection = true
}
@@ -45,15 +47,16 @@ internal class LocalPropertiesModifier(private val localProperties: File) {
if (localProperties.exists() && !localProperties.isFile) {
error("$localProperties is not a file!")
}
val content = getUserConfiguredPropertiesContent()
val manuallyConfiguredProperties = Properties().apply {
StringReader(manuallyConfiguredPropertiesContent).use {
StringReader(content).use {
load(it)
}
}
val propertiesToSetup = setupFile.properties.mapValues { PropertyValue(it.value, manuallyConfiguredProperties.containsKey(it.key)) }
localProperties.writeText(
"""
|${manuallyConfiguredPropertiesContent.addSuffix("\n")}
|${content.addSuffix("\n")}
|$SYNCED_PROPERTIES_START_LINES
|${propertiesToSetup.asPropertiesLines}
|$SYNCED_PROPERTIES_END_LINE
@@ -61,6 +64,10 @@ internal class LocalPropertiesModifier(private val localProperties: File) {
""".trimMargin()
)
}
fun initiallyContains(line: String) = initialUserConfiguredPropertiesContent.contains(line)
fun putLine(line: String) = localProperties.appendText("\n$line")
}
private fun String.addSuffix(suffix: String): String {
@@ -0,0 +1,76 @@
/*
* 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.apache.tools.ant.filters.StringInputStream
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.io.TempDir
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import java.nio.file.Files
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
class ConsentManagerTest {
@TempDir
lateinit var workingDir: Path
private val localPropertiesFile by lazy {
workingDir.resolve("local.properties")
}
private val modifier by lazy {
LocalPropertiesModifier(localPropertiesFile.toFile())
}
@Test
@DisplayName("can check if there's no decision over the consent")
fun testNoConsentRead() {
assertNull(ConsentManager(modifier).getUserDecision())
}
@Test
@DisplayName("can check if there's agree to the consent")
fun testConsentAgreeRead() {
localPropertiesFile.toFile().writeText(USER_CONSENT_MARKER)
assertEquals(true, ConsentManager(modifier).getUserDecision())
}
@Test
@DisplayName("can check if there's refusal to the consent")
fun testConsentRefusalRead() {
localPropertiesFile.toFile().writeText(USER_REFUSAL_MARKER)
assertEquals(false, ConsentManager(modifier).getUserDecision())
}
@Test
fun testConsentRequestAgree() = testUserDecision("yes", true, USER_CONSENT_MARKER)
@Test
fun testConsentRequestRefusal() = testUserDecision("no", false, USER_REFUSAL_MARKER)
@Test
fun testConsentRequestAnswerAfterBadInput() = testUserDecision("\nasdasd\nno", false, USER_REFUSAL_MARKER, 3)
private fun testUserDecision(prompt: String, expectedDecision: Boolean, expectedLine: String, promptCount: Int = 1) {
StringInputStream(prompt).bufferedReader().use { input ->
val outputStream = ByteArrayOutputStream(255)
PrintStream(outputStream).use { printStream ->
val consentManager = ConsentManager(modifier, input, printStream)
val userDecision = consentManager.askForConsent()
assertEquals(expectedDecision, userDecision)
val content = Files.readAllLines(localPropertiesFile)
assertTrue(content.contains(expectedLine))
val output = String(outputStream.toByteArray())
assertContainsExactTimes(output, USER_CONSENT_REQUEST, 1)
assertContainsExactTimes(output, PROMPT_REQUEST, promptCount)
}
}
}
}
@@ -205,21 +205,8 @@ class LocalPropertiesModifierTest {
}
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)
assertContainsExactTimes(content, SYNCED_PROPERTIES_START_LINES, 1)
assertContainsExactTimes(content, SYNCED_PROPERTIES_END_LINE, 1)
}
private fun fillInitialLocalPropertiesFile(content: Map<String, PropertyValue>) {
@@ -0,0 +1,25 @@
/*
* 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
internal fun assertContainsExactTimes(content: String, substring: String, expectedCount: Int) {
var currentOffset = 0
var count = 0
var nextIndex = content.indexOf(substring, currentOffset)
while (nextIndex != -1 && count < expectedCount + 1) {
count++
currentOffset = nextIndex + substring.length
nextIndex = content.indexOf(substring, currentOffset)
}
assert(expectedCount == count) {
"""
|The content is expected to contain '$substring' exactly $expectedCount times, but was found $count times:
|File content:
|${content.prependIndent()}
""".trimMargin()
}
}