Skip to content
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

Test Runner Implementation #19

Merged
merged 3 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.yaml:snakeyaml:2.2")
}

// Apply a specific Java toolchain to ease working on different environments.
Expand All @@ -83,3 +84,15 @@ tasks.named<Test>("test") {
showStandardStreams = true
}
}

tasks.register<JavaExec>("run-test") {
classpath = sourceSets.main.get().runtimeClasspath
mainClass = "com.featurevisor.cli.TestExecuter"

if (project.hasProperty("args")) {
val argsList = project.property("args") as String
val argsArray = argsList.split("\\s+".toRegex()).toTypedArray()
args(*argsArray)
}
}

81 changes: 81 additions & 0 deletions src/main/kotlin/com/featurevisor/cli/AssertionHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.featurevisor.cli
Tan108 marked this conversation as resolved.
Show resolved Hide resolved

import com.featurevisor.sdk.*
import com.featurevisor.types.FeatureAssertion
import com.featurevisor.types.VariableValue

internal fun assertExpectedToBeEnabled(actualValue: Boolean?, expectedValue: Boolean?) =
(actualValue ?: false) == (expectedValue ?: false)

internal fun assertVariables(featureName: String, assertion: FeatureAssertion, featurevisorInstance: FeaturevisorInstance?) =
assertion.expectedVariables?.map { (key, value) ->
when (value) {
is VariableValue.BooleanValue -> {
val actualValue = featurevisorInstance?.getVariableBoolean(
featureKey = featureName,
variableKey = key,
context = assertion.context
)
value.value == actualValue
}

is VariableValue.IntValue -> {
value.value == featurevisorInstance?.getVariableInteger(
featureKey = featureName,
variableKey = key,
context = assertion.context
)
}

is VariableValue.DoubleValue -> {
value.value == featurevisorInstance?.getVariableDouble(
featureKey = featureName,
variableKey = key,
context = assertion.context
)
}

is VariableValue.StringValue -> {
value.value == featurevisorInstance?.getVariableString(
featureKey = featureName,
variableKey = key,
context = assertion.context
)
}

is VariableValue.ArrayValue -> {
val variableValue = featurevisorInstance?.getVariableArray(
featureKey = featureName,
variableKey = key,
context = assertion.context
)

if ((variableValue as List<*>).isEmpty()) {
true
} else {
variableValue == value.values
}
}

is VariableValue.JsonValue -> {
val variableValue = featurevisorInstance?.getVariable(
featureKey = featureName,
variableKey = key,
context = assertion.context
)
(variableValue as VariableValue.JsonValue).value.equals(value.toString(), true)
}

is VariableValue.ObjectValue -> {
val variableValue = featurevisorInstance?.getVariable(
featureKey = featureName,
variableKey = key,
context = assertion.context
)
variableValue == value.value
}
}
}

internal fun assertVariation(actualVariation: String?, expectedVariation: String?) =
actualVariation?.equals(expectedVariation, true) ?: false
31 changes: 31 additions & 0 deletions src/main/kotlin/com/featurevisor/cli/CommandExecuter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.featurevisor.cli

import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit

internal fun getJsonForFeatureUsingCommand(featureName: String, environment: String, projectRootPath: String) =
try {
createCommand(featureName, environment).runCommand(getFileForSpecificPath(projectRootPath))
} catch (e: Exception) {
printMessageInRedColor("Exception in Commandline execution --> ${e.message}")
null
}

private fun createCommand(featureName: String, environment: String) =
"npx featurevisor build --feature=$featureName --environment=$environment --print --pretty"
Tan108 marked this conversation as resolved.
Show resolved Hide resolved

private fun String.runCommand(workingDir: File): String? =
try {
val parts = this.split("\\s".toRegex())
val process = ProcessBuilder(*parts.toTypedArray())
.directory(workingDir)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
.start()
process.waitFor(60, TimeUnit.MINUTES)
process.inputStream.bufferedReader().readText()
} catch (e: IOException) {
printMessageInRedColor("Exception while executing command -> ${e.message}")
null
}
169 changes: 169 additions & 0 deletions src/main/kotlin/com/featurevisor/cli/Matrix.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package com.featurevisor.cli

import com.featurevisor.types.*

fun generateCombinations(
keys: List<String>,
matrix: AssertionMatrix,
idx: Int,
prev: MutableMap<String, Any>,
combinations: MutableList<MutableMap<String, Any>>
) {
val key = keys[idx]
val values = matrix[key] ?: emptyList()

for (i in values.indices) {
val combination = prev.toMutableMap().apply { put(key, values[i]) }

if (idx == keys.size - 1) {
combinations.add(combination)
} else {
generateCombinations(keys, matrix, idx + 1, combination, combinations)
}
}
}

fun getMatrixCombinations(matrix: AssertionMatrix): List<MutableMap<String, Any>> {
val keys = matrix.keys.toList()

if (keys.isEmpty()) {
return emptyList()
}

val combinations = mutableListOf<MutableMap<String, Any>>()
generateCombinations(keys, matrix, 0, mutableMapOf(), combinations)
return combinations
}

fun applyCombinationToValue(value: Any?, combination: Map<String, Any>): Any? {
if (value is String) {
val variableKeysInValue = Regex("""\$\{\{\s*([^\s}]+)\s*}}""").findAll(value)

if (variableKeysInValue.none()) {
return value
}



return variableKeysInValue.fold(value) { acc, result ->
val key = result.groupValues[1].trim()
val regex = Regex("""\$\{\{\s*([^\s}]+)\s*}}""")
acc.replace(regex, combination[key].toString())
}
}
return value
}

fun applyCombinationToFeatureAssertion(
combination: Map<String, Any>,
assertion: FeatureAssertion
): FeatureAssertion {
val flattenedAssertion = assertion.copy()

flattenedAssertion.environment = applyCombinationToValue(
flattenedAssertion.environment,
combination
) as EnvironmentKey

flattenedAssertion.context =
flattenedAssertion.context.mapValues { (_, value) ->
getContextValue(applyCombinationToValue(getContextValues(value), combination))
} as Context

flattenedAssertion.at = applyCombinationToValue(getAtValue(flattenedAssertion.at).toString(), combination)?.let {
if (it is String) {
if (it.contains(".")){
WeightType.DoubleType( it.toDouble())
}else{
WeightType.IntType(it.toInt())
}
} else it
} as WeightType

flattenedAssertion.description = applyCombinationToValue(
flattenedAssertion.description,
combination
) as? String

return flattenedAssertion
}

fun getFeatureAssertionsFromMatrix(
aIndex: Int,
assertionWithMatrix: FeatureAssertion
): List<FeatureAssertion> {
if (assertionWithMatrix.matrix == null) {
val assertion = assertionWithMatrix.copy()
assertion.description = "Assertion #${aIndex + 1}: (${assertion.environment}) ${
assertion.description ?: "at ${getAtValue(assertion.at)}%"}"
return listOf(assertion)
}

val assertions = mutableListOf<FeatureAssertion>()
val combinations = getMatrixCombinations(assertionWithMatrix.matrix)

for (combination in combinations) {
val assertion = applyCombinationToFeatureAssertion(combination, assertionWithMatrix)
assertion.description = "Assertion #${aIndex + 1}: (${assertion.environment}) ${
assertion.description ?: "at ${getAtValue(assertion.at)}%"
}"
assertions.add(assertion)
}

return assertions
}

@Suppress("IMPLICIT_CAST_TO_ANY")
fun getAtValue(at:WeightType) = when (at) {
is WeightType.IntType -> {
at.value
}

is WeightType.DoubleType -> {
at.value
}

is WeightType.StringType -> {
at.value
}
}

fun applyCombinationToSegmentAssertion(
combination: Map<String, Any>,
assertion: SegmentAssertion
): SegmentAssertion {
val flattenedAssertion = assertion.copy()

flattenedAssertion.context = flattenedAssertion.context.mapValues { (_, value) ->
getContextValue(applyCombinationToValue(getContextValues(value), combination))
} as Context

flattenedAssertion.description = applyCombinationToValue(
flattenedAssertion.description,
combination
) as? String

return flattenedAssertion
}

fun getSegmentAssertionsFromMatrix(
aIndex: Int,
assertionWithMatrix: SegmentAssertion
): List<SegmentAssertion> {
if (assertionWithMatrix.matrix == null) {
val assertion = assertionWithMatrix.copy()
assertion.description = "Assertion #${aIndex + 1}: ${assertion.description ?: "#${aIndex + 1}"}"
return listOf(assertion)
}

val assertions = mutableListOf<SegmentAssertion>()
val combinations = getMatrixCombinations(assertionWithMatrix.matrix)

for (combination in combinations) {
val assertion = applyCombinationToSegmentAssertion(combination, assertionWithMatrix)
assertion.description = "Assertion #${aIndex + 1}: ${assertion.description ?: "#${aIndex + 1}"}"
assertions.add(assertion)
}

return assertions
}
Loading
Loading