diff --git a/theodolite-quarkus/build.gradle b/theodolite-quarkus/build.gradle index 8b61bf506dd241c5b0d4e75b1357ef3fd5966135..8c0a13d1f24cfde01c8604285c49d3f48fe6a3b7 100644 --- a/theodolite-quarkus/build.gradle +++ b/theodolite-quarkus/build.gradle @@ -25,9 +25,12 @@ dependencies { implementation 'io.quarkus:quarkus-kubernetes-client' implementation 'org.apache.kafka:kafka-clients:2.7.0' implementation 'khttp:khttp:1.0.0' + compile 'junit:junit:4.12' + testImplementation 'io.quarkus:quarkus-junit5' testImplementation 'io.rest-assured:rest-assured' + testImplementation 'org.junit-pioneer:junit-pioneer:1.4.0' } group 'theodolite' diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt index 59a793bd7c6d2897fc715c58deda54c178d160f4..ef4d371173c7099eb091f90cddbe26d31e6522be 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/AnalysisExecutor.kt @@ -2,6 +2,7 @@ package theodolite.evaluation import mu.KotlinLogging import theodolite.benchmark.BenchmarkExecution +import theodolite.util.IOHandler import theodolite.util.LoadDimension import theodolite.util.Resource import java.text.Normalizer @@ -36,24 +37,25 @@ class AnalysisExecutor( */ fun analyze(load: LoadDimension, res: Resource, executionIntervals: List<Pair<Instant, Instant>>): Boolean { var result = false - val exporter = CsvExporter() var repetitionCounter = 1 try { - var resultsFolder: String = System.getenv("RESULTS_FOLDER") ?: "" - if (resultsFolder.isNotEmpty()){ - resultsFolder += "/" - } + val ioHandler = IOHandler() + val resultsFolder: String = ioHandler.getResultFolderURL() + val fileURL = "${resultsFolder}exp${executionId}_${load.get()}_${res.get()}_${slo.sloType.toSlug()}" + val prometheusData = executionIntervals .map { interval -> fetcher.fetchMetric( start = interval.first, end = interval.second, query = "sum by(group)(kafka_consumergroup_group_lag >= 0)") } - val fileName= "${resultsFolder}exp${executionId}_${load.get()}_${res.get()}_${slo.sloType.toSlug()}" prometheusData.forEach{ data -> - exporter.toCsv(name = "${fileName}_${repetitionCounter++}", prom = data) } - + ioHandler.writeToCSVFile( + fileURL = "${fileURL}_${repetitionCounter++}", + data = data.getResultAsList(), + columns = listOf("group", "timestamp", "value")) + } val sloChecker = SloCheckerFactory().create( sloType = slo.sloType, diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/CsvExporter.kt b/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/CsvExporter.kt deleted file mode 100644 index 970d1487014bfbddf7391e381d27ad5dbb246a7d..0000000000000000000000000000000000000000 --- a/theodolite-quarkus/src/main/kotlin/theodolite/evaluation/CsvExporter.kt +++ /dev/null @@ -1,53 +0,0 @@ -package theodolite.evaluation - -import mu.KotlinLogging -import theodolite.util.PrometheusResponse -import java.io.File -import java.io.PrintWriter -import java.util.* - -private val logger = KotlinLogging.logger {} - -/** - * Used to document the data received from prometheus for additional offline analysis. - */ -class CsvExporter { - - /** - * Uses the [PrintWriter] to transform a [PrometheusResponse] to a CSV file. - * @param name of the file. - * @param prom Response that is documented. - * - */ - fun toCsv(name: String, prom: PrometheusResponse) { - val responseArray = promResponseToList(prom) - val csvOutputFile = File("$name.csv") - - PrintWriter(csvOutputFile).use { pw -> - pw.println(listOf("group", "timestamp", "value").joinToString(separator=",")) - responseArray.forEach { - pw.println(it.joinToString(separator=",")) - } - } - logger.info { "Wrote CSV file: $name to ${csvOutputFile.absolutePath}." } - } - - /** - * Converts a [PrometheusResponse] into a [List] of [List]s of [String]s - */ - private fun promResponseToList(prom: PrometheusResponse): List<List<String>> { - val name = prom.data?.result?.get(0)?.metric?.group.toString() - val values = prom.data?.result?.get(0)?.values - val dataList = mutableListOf<List<String>>() - - if (values != null) { - for (maybeValuePair in values) { - val valuePair = maybeValuePair as List<*> - val timestamp = (valuePair[0] as Double).toLong().toString() - val value = valuePair[1].toString() - dataList.add(listOf(name, timestamp, value)) - } - } - return Collections.unmodifiableList(dataList) - } -} diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt index 1fecc4664b02e51b581d84d6cbc19c9ddda82c9a..d38b50b70c63c90e6bbb618386e0ed897087e6f1 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/execution/TheodoliteExecutor.kt @@ -1,18 +1,13 @@ package theodolite.execution -import com.google.gson.GsonBuilder import mu.KotlinLogging import theodolite.benchmark.BenchmarkExecution import theodolite.benchmark.KubernetesBenchmark import theodolite.patcher.PatcherDefinitionFactory import theodolite.strategies.StrategyFactory import theodolite.strategies.searchstrategy.CompositeStrategy -import theodolite.util.Config -import theodolite.util.LoadDimension -import theodolite.util.Resource -import theodolite.util.Results +import theodolite.util.* import java.io.File -import java.io.PrintWriter import java.time.Duration @@ -100,34 +95,16 @@ class TheodoliteExecutor( return this.kubernetesBenchmark } - private fun getResultFolderString(): String { - var resultsFolder: String = System.getenv("RESULTS_FOLDER") ?: "" - val createResultsFolder = System.getenv("CREATE_RESULTS_FOLDER") ?: "false" - - if (resultsFolder != ""){ - logger.info { "RESULT_FOLDER: $resultsFolder" } - val directory = File(resultsFolder) - if (!directory.exists()) { - logger.error { "Folder $resultsFolder does not exist" } - if (createResultsFolder.toBoolean()) { - directory.mkdirs() - } else { - throw IllegalArgumentException("Result folder not found") - } - } - resultsFolder += "/" - } - return resultsFolder - } - /** * Run all experiments which are specified in the corresponding * execution and benchmark objects. */ fun run() { - val resultsFolder = getResultFolderString() - storeAsFile(this.config, "$resultsFolder${this.config.executionId}-execution-configuration") - storeAsFile(kubernetesBenchmark, "$resultsFolder${this.config.executionId}-benchmark-configuration") + val ioHandler = IOHandler() + val resultsFolder = ioHandler.getResultFolderURL() + this.config.executionId = getAndIncrementExecutionID(resultsFolder+"expID.txt") + ioHandler.writeToJSONFile(this.config, "$resultsFolder${this.config.executionId}-execution-configuration") + ioHandler.writeToJSONFile(kubernetesBenchmark, "$resultsFolder${this.config.executionId}-benchmark-configuration") val config = buildConfig() // execute benchmarks for each load @@ -136,14 +113,17 @@ class TheodoliteExecutor( config.compositeStrategy.findSuitableResource(load, config.resources) } } - storeAsFile(config.compositeStrategy.benchmarkExecutor.results, "$resultsFolder${this.config.executionId}-result") + ioHandler.writeToJSONFile(config.compositeStrategy.benchmarkExecutor.results, "$resultsFolder${this.config.executionId}-result") } - private fun <T> storeAsFile(saveObject: T, filename: String) { - val gson = GsonBuilder().enableComplexMapKeySerialization().setPrettyPrinting().create() - - PrintWriter(filename).use { pw -> - pw.println(gson.toJson(saveObject)) - } + private fun getAndIncrementExecutionID(fileURL: String): Int { + val ioHandler = IOHandler() + var executionID = 0 + if (File(fileURL).exists()) { + executionID = ioHandler.readFileAsString(fileURL).toInt() + 1 + } + ioHandler.writeStringToTextFile(fileURL, (executionID).toString()) + return executionID } + } diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/util/IOHandler.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/IOHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d379fcf0543257edafd2e45383a02ba0254563d --- /dev/null +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/IOHandler.kt @@ -0,0 +1,94 @@ +package theodolite.util + +import com.google.gson.GsonBuilder +import mu.KotlinLogging +import java.io.File +import java.io.PrintWriter + +private val logger = KotlinLogging.logger {} + +/** + * The IOHandler handles most common I/O operations within the Theodolite framework + */ +class IOHandler { + + /** + * The location in which Theodolite store result and configuration file are depends on + * the values of the environment variables `RESULT_FOLDER` and `CREATE_RESULTS_FOLDER` + * + * @return the URL of the result folder + */ + fun getResultFolderURL(): String { + var resultsFolder: String = System.getenv("RESULTS_FOLDER") ?: "" + val createResultsFolder = System.getenv("CREATE_RESULTS_FOLDER") ?: "false" + + if (resultsFolder != ""){ + logger.info { "RESULT_FOLDER: $resultsFolder" } + val directory = File(resultsFolder) + if (!directory.exists()) { + logger.error { "Folder $resultsFolder does not exist" } + if (createResultsFolder.toBoolean()) { + directory.mkdirs() + } else { + throw IllegalArgumentException("Result folder not found") + } + } + resultsFolder += "/" + } + return resultsFolder + } + + /** + * Read a file as String + * + * @param fileURL the URL of the file + * @return The content of the file as String + */ + fun readFileAsString(fileURL: String): String { + return File(fileURL).inputStream().readBytes().toString(Charsets.UTF_8).trim() + } + + /** + * Creates a JSON string of the given object and store them to file + * + * @param T class of the object to save + * @param objectToSave object which should be saved as file + * @param fileURL the URL of the file + */ + fun <T> writeToJSONFile(objectToSave: T, fileURL: String) { + val gson = GsonBuilder().enableComplexMapKeySerialization().setPrettyPrinting().create() + writeStringToTextFile(fileURL, gson.toJson(objectToSave)) + } + + /** + * Write to CSV file + * + * @param fileURL the URL of the file + * @param data the data to write in the file, as list of list, each subList corresponds to a row in the CSV file + * @param columns columns of the CSV file + */ + fun writeToCSVFile(fileURL: String, data: List<List<String>>, columns: List<String>) { + val outputFile = File("$fileURL.csv") + PrintWriter(outputFile).use { pw -> + pw.println(columns.joinToString(separator=",")) + data.forEach { + pw.println(it.joinToString(separator=",")) + } + } + logger.info { "Wrote CSV file: $fileURL to ${outputFile.absolutePath}." } + } + + /** + * Write to text file + * + * @param fileURL the URL of the file + * @param data the data to write in the file as String + */ + fun writeStringToTextFile(fileURL: String, data: String) { + val outputFile = File("$fileURL") + outputFile.printWriter().use { + it.println(data) + } + logger.info { "Wrote txt file: $fileURL to ${outputFile.absolutePath}." } + } +} \ No newline at end of file diff --git a/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt b/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt index d1d59c482e64fd14c4744d8fcd606f286da24fb4..846577387c425e920da1c2fca1f972c880e1540a 100644 --- a/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt +++ b/theodolite-quarkus/src/main/kotlin/theodolite/util/PrometheusResponse.kt @@ -1,6 +1,7 @@ package theodolite.util import io.quarkus.runtime.annotations.RegisterForReflection +import java.util.* /** * This class corresponds to the JSON response format of a Prometheus @@ -17,6 +18,27 @@ data class PrometheusResponse( */ var data: PromData? = null ) +{ + /** + * Return the data of the PrometheusResponse as [List] of [List]s of [String]s + * The format of the returned list is: `[[ group, timestamp, value ], [ group, timestamp, value ], ... ]` + */ + fun getResultAsList(): List<List<String>> { + val group = data?.result?.get(0)?.metric?.group.toString() + val values = data?.result?.get(0)?.values + val result = mutableListOf<List<String>>() + + if (values != null) { + for (value in values) { + val valueList = value as List<*> + val timestamp = (valueList[0] as Double).toLong().toString() + val value = valueList[1].toString() + result.add(listOf(group, timestamp, value)) + } + } + return Collections.unmodifiableList(result) + } +} /** * Description of Prometheus data. @@ -56,4 +78,5 @@ data class PromResult( @RegisterForReflection data class PromMetric( var group: String? = null -) \ No newline at end of file +) + diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt index 82e4bc5d77f3f35d217c56a377513c0e7d329170..4f4308a35df598dc4932a8ac89a6c07fb967e416 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceLimitPatcherTest.kt @@ -21,7 +21,7 @@ import theodolite.util.PatcherDefinition */ @QuarkusTest class ResourceLimitPatcherTest { - val testPath = "./src/main/resources/testYaml/" + val testPath = "./src/test/resources/" val loader = K8sResourceLoader(DefaultKubernetesClient().inNamespace("")) val patcherFactory = PatcherFactory() diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt index 3cd6b012f09c5471b1b011b5cd03e61a0fab1c4e..7214422705a64f507a196a4b5f9b334665407422 100644 --- a/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt +++ b/theodolite-quarkus/src/test/kotlin/theodolite/ResourceRequestPatcherTest.kt @@ -21,7 +21,7 @@ import theodolite.util.PatcherDefinition */ @QuarkusTest class ResourceRequestPatcherTest { - val testPath = "./src/main/resources/testYaml/" + val testPath = "./src/test/resources/" val loader = K8sResourceLoader(DefaultKubernetesClient().inNamespace("")) val patcherFactory = PatcherFactory() diff --git a/theodolite-quarkus/src/test/kotlin/theodolite/util/IOHandlerTest.kt b/theodolite-quarkus/src/test/kotlin/theodolite/util/IOHandlerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..6984c3935da3d26f95565b758547a98e0eeb155f --- /dev/null +++ b/theodolite-quarkus/src/test/kotlin/theodolite/util/IOHandlerTest.kt @@ -0,0 +1,134 @@ +package theodolite.util + +import com.google.gson.GsonBuilder +import io.quarkus.test.junit.QuarkusTest +import org.hamcrest.CoreMatchers.containsString +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Rule +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.rules.TemporaryFolder +import org.junitpioneer.jupiter.ClearEnvironmentVariable +import org.junitpioneer.jupiter.SetEnvironmentVariable + + +const val FOLDER_URL = "Test-Folder" +@QuarkusTest +internal class IOHandlerTest { + + @Rule + private var temporaryFolder = TemporaryFolder() + + @Test + fun testWriteStringToText() { + temporaryFolder.create() + val testContent = "Test-File-Content" + val folder = temporaryFolder.newFolder(FOLDER_URL) + + IOHandler().writeStringToTextFile( + fileURL = "${folder.absolutePath}/test-file.txt", + data = testContent) + + assertEquals( + testContent, + IOHandler().readFileAsString("${folder.absolutePath}/test-file.txt") + ) + } + + @Test + fun testWriteToCSVFile() { + temporaryFolder.create() + val folder = temporaryFolder.newFolder(FOLDER_URL) + + val testContent = listOf( + listOf("apples","red"), + listOf("bananas","yellow"), + listOf("avocado","brown")) + val columns = listOf("Fruit", "Color") + + IOHandler().writeToCSVFile( + fileURL = "${folder.absolutePath}/test-file", + data = testContent, + columns = columns) + + var expected = "Fruit,Color\n" + testContent.forEach { expected += it[0] + "," + it[1] + "\n" } + + assertEquals( + expected.trim(), + IOHandler().readFileAsString("${folder.absolutePath}/test-file.csv") + ) + } + + @Test + fun testWriteToJSONFile() { + temporaryFolder.create() + val folder = temporaryFolder.newFolder(FOLDER_URL) + val testContent = Resource(0, emptyList()) + + IOHandler().writeToJSONFile( + fileURL = "${folder.absolutePath}/test-file.json", + objectToSave = testContent) + + val expected = GsonBuilder().enableComplexMapKeySerialization().setPrettyPrinting().create().toJson(testContent) + + assertEquals( + expected, + IOHandler().readFileAsString("${folder.absolutePath}/test-file.json") + ) + } + + // Test the function `getResultFolderString` + + @Test + @ClearEnvironmentVariable.ClearEnvironmentVariables( + ClearEnvironmentVariable(key = "RESULTS_FOLDER"), + ClearEnvironmentVariable(key = "CREATE_RESULTS_FOLDER") + ) + fun testGetResultFolderURL_emptyEnvironmentVars() { + assertEquals("",IOHandler().getResultFolderURL()) + } + + + @Test() + @SetEnvironmentVariable.SetEnvironmentVariables( + SetEnvironmentVariable(key = "RESULTS_FOLDER", value = "./src/test/resources"), + SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "false") + ) + fun testGetResultFolderURL_FolderExist() { + assertEquals("./src/test/resources/", IOHandler().getResultFolderURL()) + } + + @Test() + @SetEnvironmentVariable.SetEnvironmentVariables( + SetEnvironmentVariable(key = "RESULTS_FOLDER", value = "$FOLDER_URL-0"), + SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "false") + ) + fun testGetResultFolderURL_FolderNotExist() { + var exceptionWasThrown = false + try { + IOHandler().getResultFolderURL() + } catch (e: Exception){ + exceptionWasThrown = true + assertThat(e.toString(), containsString("Result folder not found")); + } + assertTrue(exceptionWasThrown) + } + + @Test() + @SetEnvironmentVariable.SetEnvironmentVariables( + SetEnvironmentVariable(key = "RESULTS_FOLDER", value = FOLDER_URL), + SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "true") + ) + fun testGetResultFolderURL_CreateFolderIfNotExist() { + assertEquals("$FOLDER_URL/", IOHandler().getResultFolderURL()) + } + + @Test() + @ClearEnvironmentVariable(key = "RESULTS_FOLDER" ) + @SetEnvironmentVariable(key = "CREATE_RESULTS_FOLDER", value = "true") + fun testGetResultFolderURL_CreateFolderButNoFolderGiven() { + assertEquals("", IOHandler().getResultFolderURL()) + } +} diff --git a/theodolite-quarkus/src/main/resources/testYaml/cpu-deployment.yaml b/theodolite-quarkus/src/test/resources/cpu-deployment.yaml similarity index 100% rename from theodolite-quarkus/src/main/resources/testYaml/cpu-deployment.yaml rename to theodolite-quarkus/src/test/resources/cpu-deployment.yaml diff --git a/theodolite-quarkus/src/main/resources/testYaml/cpu-memory-deployment.yaml b/theodolite-quarkus/src/test/resources/cpu-memory-deployment.yaml similarity index 100% rename from theodolite-quarkus/src/main/resources/testYaml/cpu-memory-deployment.yaml rename to theodolite-quarkus/src/test/resources/cpu-memory-deployment.yaml diff --git a/theodolite-quarkus/src/main/resources/testYaml/memory-deployment.yaml b/theodolite-quarkus/src/test/resources/memory-deployment.yaml similarity index 100% rename from theodolite-quarkus/src/main/resources/testYaml/memory-deployment.yaml rename to theodolite-quarkus/src/test/resources/memory-deployment.yaml diff --git a/theodolite-quarkus/src/main/resources/testYaml/no-resources-deployment.yaml b/theodolite-quarkus/src/test/resources/no-resources-deployment.yaml similarity index 100% rename from theodolite-quarkus/src/main/resources/testYaml/no-resources-deployment.yaml rename to theodolite-quarkus/src/test/resources/no-resources-deployment.yaml