From 5f8b078366f948a96e23cdcd921e143752e4ebca Mon Sep 17 00:00:00 2001 From: Tanmay Ranjan Date: Fri, 8 Mar 2024 15:09:30 +0530 Subject: [PATCH] sdk fixes in all scenarios --- .../com/featurevisor/cli/AssertionHelper.kt | 81 ------- .../com/featurevisor/cli/TestFeature.kt | 198 ---------------- .../featurevisor/sdk/Instance+Evaluation.kt | 34 ++- .../sdk/serializers/Serializers.kt | 26 +-- .../{cli => testRunner}/CommandExecuter.kt | 2 +- .../{cli => testRunner}/Matrix.kt | 17 +- .../{cli => testRunner}/Parser.kt | 91 ++++---- .../{cli => testRunner}/TestExecuter.kt | 3 +- .../featurevisor/testRunner/TestFeature.kt | 218 ++++++++++++++++++ .../{cli => testRunner}/TestSegment.kt | 11 +- .../featurevisor/{cli => testRunner}/Utils.kt | 30 +-- 11 files changed, 333 insertions(+), 378 deletions(-) delete mode 100644 src/main/kotlin/com/featurevisor/cli/AssertionHelper.kt delete mode 100644 src/main/kotlin/com/featurevisor/cli/TestFeature.kt rename src/main/kotlin/com/featurevisor/{cli => testRunner}/CommandExecuter.kt (97%) rename src/main/kotlin/com/featurevisor/{cli => testRunner}/Matrix.kt (95%) rename src/main/kotlin/com/featurevisor/{cli => testRunner}/Parser.kt (67%) rename src/main/kotlin/com/featurevisor/{cli => testRunner}/TestExecuter.kt (99%) create mode 100644 src/main/kotlin/com/featurevisor/testRunner/TestFeature.kt rename src/main/kotlin/com/featurevisor/{cli => testRunner}/TestSegment.kt (82%) rename src/main/kotlin/com/featurevisor/{cli => testRunner}/Utils.kt (89%) diff --git a/src/main/kotlin/com/featurevisor/cli/AssertionHelper.kt b/src/main/kotlin/com/featurevisor/cli/AssertionHelper.kt deleted file mode 100644 index ce0b116..0000000 --- a/src/main/kotlin/com/featurevisor/cli/AssertionHelper.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.featurevisor.cli - -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 diff --git a/src/main/kotlin/com/featurevisor/cli/TestFeature.kt b/src/main/kotlin/com/featurevisor/cli/TestFeature.kt deleted file mode 100644 index 5cc8311..0000000 --- a/src/main/kotlin/com/featurevisor/cli/TestFeature.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.featurevisor.cli - -import com.featurevisor.sdk.getVariable -import com.featurevisor.sdk.getVariation -import com.featurevisor.sdk.isEnabled -import com.featurevisor.types.* -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement - -fun testFeature(testFeature: TestFeature, projectRootPath: String): TestResult { - val featureKey = testFeature.key - - val testResult = TestResult( - type = "feature", - key = featureKey, - notFound = false, - duration = 0, - passed = true, - assertions = mutableListOf() - ) - - testFeature.assertions.forEachIndexed{ index, assertion -> - val assertions = getFeatureAssertionsFromMatrix(index, assertion) - - assertions.forEach { - - val testResultAssertion = TestResultAssertion( - description = it.description.orEmpty(), - environment = it.environment, - duration = 0, - passed = true, - errors = mutableListOf() - ) - - - val datafileContent = getDataFileContent( - featureName = testFeature.key, - environment = it.environment, - projectRootPath = projectRootPath - ) - - if (datafileContent != null) { - - val featurevisorInstance = getSdkInstance(datafileContent, it) - - if (testFeature.key.isEmpty()) { - testResult.notFound = true - testResult.passed = false - - return testResult - } - - // isEnabled - if (it.expectedToBeEnabled != null){ - val isEnabled = featurevisorInstance.isEnabled(testFeature.key, it.context) - - if(isEnabled != it.expectedToBeEnabled){ - testResult.passed = false - testResultAssertion.passed = false - - (testResultAssertion.errors as MutableList).add( - TestResultAssertionError( - type = "flag", - expected = it.expectedToBeEnabled, - actual = isEnabled - ) - ) - } - } - - //Variation - if (!it.expectedVariation.isNullOrEmpty()){ - val variation = featurevisorInstance.getVariation(testFeature.key, it.context) - - if ( variation != it.expectedVariation){ - testResult.passed = false - testResultAssertion.passed = false - - (testResultAssertion.errors as MutableList).add( - TestResultAssertionError( - type = "variation", - expected = it.expectedVariation, - actual = variation - ) - ) - } - } - - //Variables - if (assertion.expectedVariables is Map<*, *>){ - - assertion.expectedVariables.forEach{ (variableKey, expectedValue) -> - val actualValue = featurevisorInstance.getVariable(featureKey, variableKey, it.context) - val passed: Boolean - - val variableSchema = datafileContent.features.find { feature -> - feature.key == testFeature.key - }?.variablesSchema?.find { variableSchema -> - variableSchema.key.equals(variableKey, ignoreCase = true) - } - - if(variableSchema == null){ - testResult.passed = false - testResultAssertion.passed = false - - - (testResultAssertion.errors as MutableList).add( - TestResultAssertionError( - type = "variable", - expected = it.expectedVariation, - actual = null, - message = "schema for variable \"${variableKey}\" not found in feature" - ) - ) - return@forEach - } - - - if (variableSchema.type == VariableType.JSON) { - // JSON type - val parsedExpectedValue = if (expectedValue is VariableValue.StringValue) { - try { - Json.decodeFromString>(expectedValue.value) - } catch (e: Exception) { - expectedValue - } - } else { - expectedValue - } - - passed = when (actualValue) { - is VariableValue.ArrayValue -> checkIfArraysAreEqual(stringToArray(parsedExpectedValue.toString()).orEmpty().toTypedArray(), actualValue.values.toTypedArray()) - is VariableValue.ObjectValue -> checkIfObjectsAreEqual((parsedExpectedValue as VariableValue.ObjectValue).value, (actualValue as VariableValue.ObjectValue).value) - is VariableValue.JsonValue -> checkMapIsEquals((expectedValue as VariableValue.JsonValue).value, (actualValue as VariableValue.JsonValue).value) - else -> parsedExpectedValue == actualValue - } - - if (!passed) { - testResult.passed = false - testResultAssertion.passed = false - - val expectedValueString = if (expectedValue !is VariableValue.StringValue) expectedValue.toString() else expectedValue - val actualValueString = if (actualValue !is VariableValue.StringValue) actualValue.toString() else actualValue - - (testResultAssertion.errors as MutableList).add( - TestResultAssertionError( - type = "variable", - expected = expectedValueString, - actual = actualValueString, - details = mapOf("variableKey" to variableKey) - ) - ) - } - }else{ - passed = when (expectedValue) { - is VariableValue.ArrayValue -> checkIfArraysAreEqual((expectedValue as VariableValue.ArrayValue).values.toTypedArray(), (actualValue as VariableValue.ArrayValue).values.toTypedArray()) - is VariableValue.ObjectValue -> checkIfObjectsAreEqual(expectedValue, actualValue) - else -> expectedValue == actualValue - } - - if (!passed) { - testResult.passed = false - testResultAssertion.passed = false - - val expectedValueString = if (expectedValue !is VariableValue.StringValue) expectedValue.toString() else expectedValue - val actualValueString = if (actualValue !is VariableValue.StringValue) actualValue.toString() else actualValue - - (testResultAssertion.errors as MutableList).add( - TestResultAssertionError( - type = "variable", - expected = expectedValueString, - actual = actualValueString, - details = mapOf("variableKey" to variableKey) - ) - ) - } - } - } - } - }else{ - testResult.passed = false - testResultAssertion.passed = false - - (testResultAssertion.errors as MutableList).add( - TestResultAssertionError( - type = "Data File", - expected = null, - actual = null, - message = "Unable to generate Data File" - ) - ) - } - (testResult.assertions as MutableList).add(testResultAssertion) - } - } - return testResult -} diff --git a/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt b/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt index 7294418..6539442 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt @@ -286,9 +286,9 @@ fun FeaturevisorInstance.evaluateFlag(featureKey: FeatureKey, context: Context = // required if (feature.required.isNullOrEmpty().not()) { - val requiredFeaturesAreEnabled = feature.required!!.all { item -> - var requiredKey: FeatureKey - var requiredVariation: VariationValue? + val requiredFeaturesAreEnabled = feature.required?.all { item -> + var requiredKey: FeatureKey? = null + var requiredVariation: VariationValue? = null when (item) { is Required.FeatureKey -> { requiredKey = item.required @@ -307,12 +307,16 @@ fun FeaturevisorInstance.evaluateFlag(featureKey: FeatureKey, context: Context = return@all false } - val requiredVariationValue = getVariation(requiredKey, finalContext) + if (requiredVariation != null){ + val requiredVariationValue = getVariation(requiredKey, finalContext) - return@all requiredVariationValue == requiredVariation + return@all requiredVariationValue == requiredVariation + } + + return@all true } - if (requiredFeaturesAreEnabled.not()) { + if ((requiredFeaturesAreEnabled == false)) { evaluation = Evaluation( featureKey = feature.key, reason = REQUIRED, @@ -482,9 +486,11 @@ fun FeaturevisorInstance.evaluateVariable( val finalContext = interceptContext?.invoke(context) ?: context // forced - findForceFromFeature(feature, context, datafileReader)?.let { force -> - if (force.variables?.containsKey(variableKey) == true) { - val variableValue = force.variables[variableKey] + val force = findForceFromFeature(feature, context, datafileReader) + + force?.let { + if (it.variables?.containsKey(variableKey) == true) { + val variableValue = it.variables[variableKey] evaluation = Evaluation( featureKey = feature.key, reason = FORCED, @@ -500,6 +506,7 @@ fun FeaturevisorInstance.evaluateVariable( // bucketing val bucketValue = getBucketValue(feature, finalContext) + val matchedTrafficAndAllocation = getMatchedTrafficAndAllocation( traffic = feature.traffic, context = finalContext, @@ -528,8 +535,15 @@ fun FeaturevisorInstance.evaluateVariable( // regular allocation matchedTrafficAndAllocation.matchedAllocation?.let { matchedAllocation -> + + val variationValue: String = if (force?.variation != null) { + force.variation + } else { + matchedAllocation.variation + } + val variation = feature.variations?.firstOrNull { variation -> - variation.value == matchedAllocation.variation + variation.value == variationValue } val variableFromVariation = variation?.variables?.firstOrNull { variable -> diff --git a/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt b/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt index 9c629d3..f760563 100644 --- a/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt +++ b/src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt @@ -11,11 +11,7 @@ import com.featurevisor.types.Operator import com.featurevisor.types.OrGroupSegment import com.featurevisor.types.VariableValue import com.featurevisor.types.Required -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer -import kotlinx.serialization.decodeFromString +import kotlinx.serialization.* import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildSerialDescriptor @@ -33,11 +29,9 @@ import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.serializer +import java.text.SimpleDateFormat - - -@OptIn(InternalSerializationApi::class) +@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) @Serializer(forClass = Required::class) object RequiredSerializer: KSerializer{ override val descriptor: SerialDescriptor = @@ -59,10 +53,6 @@ object RequiredSerializer: KSerializer{ } } - - - - @OptIn(InternalSerializationApi::class) @Serializer(forClass = Condition::class) object ConditionSerializer : KSerializer { @@ -276,9 +266,13 @@ object ConditionValueSerializer : KSerializer { } ?: tree.doubleOrNull?.let { ConditionValue.DoubleValue(it) } ?: tree.content.let { - ConditionValue.StringValue(it) - // TODO: -// ConditionValue.DateTimeValue + try { + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + val date = dateFormat.parse(it) + ConditionValue.DateTimeValue(date) + }catch (e:Exception){ + ConditionValue.StringValue(it) + } } } diff --git a/src/main/kotlin/com/featurevisor/cli/CommandExecuter.kt b/src/main/kotlin/com/featurevisor/testRunner/CommandExecuter.kt similarity index 97% rename from src/main/kotlin/com/featurevisor/cli/CommandExecuter.kt rename to src/main/kotlin/com/featurevisor/testRunner/CommandExecuter.kt index 56aaddd..cbcb677 100644 --- a/src/main/kotlin/com/featurevisor/cli/CommandExecuter.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/CommandExecuter.kt @@ -1,4 +1,4 @@ -package com.featurevisor.cli +package com.featurevisor.testRunner import java.io.File import java.io.IOException diff --git a/src/main/kotlin/com/featurevisor/cli/Matrix.kt b/src/main/kotlin/com/featurevisor/testRunner/Matrix.kt similarity index 95% rename from src/main/kotlin/com/featurevisor/cli/Matrix.kt rename to src/main/kotlin/com/featurevisor/testRunner/Matrix.kt index 8787eab..ab3e82d 100644 --- a/src/main/kotlin/com/featurevisor/cli/Matrix.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/Matrix.kt @@ -1,4 +1,4 @@ -package com.featurevisor.cli +package com.featurevisor.testRunner import com.featurevisor.types.* @@ -43,8 +43,6 @@ fun applyCombinationToValue(value: Any?, combination: Map): Any? { return value } - - return variableKeysInValue.fold(value) { acc, result -> val key = result.groupValues[1].trim() val regex = Regex("""\$\{\{\s*([^\s}]+)\s*}}""") @@ -68,13 +66,13 @@ fun applyCombinationToFeatureAssertion( flattenedAssertion.context = flattenedAssertion.context.mapValues { (_, value) -> getContextValue(applyCombinationToValue(getContextValues(value), combination)) - } as Context + } as Context flattenedAssertion.at = applyCombinationToValue(getAtValue(flattenedAssertion.at).toString(), combination)?.let { if (it is String) { - if (it.contains(".")){ - WeightType.DoubleType( it.toDouble()) - }else{ + if (it.contains(".")) { + WeightType.DoubleType(it.toDouble()) + } else { WeightType.IntType(it.toInt()) } } else it @@ -95,7 +93,8 @@ fun getFeatureAssertionsFromMatrix( if (assertionWithMatrix.matrix == null) { val assertion = assertionWithMatrix.copy() assertion.description = "Assertion #${aIndex + 1}: (${assertion.environment}) ${ - assertion.description ?: "at ${getAtValue(assertion.at)}%"}" + assertion.description ?: "at ${getAtValue(assertion.at)}%" + }" return listOf(assertion) } @@ -114,7 +113,7 @@ fun getFeatureAssertionsFromMatrix( } @Suppress("IMPLICIT_CAST_TO_ANY") -fun getAtValue(at:WeightType) = when (at) { +fun getAtValue(at: WeightType) = when (at) { is WeightType.IntType -> { at.value } diff --git a/src/main/kotlin/com/featurevisor/cli/Parser.kt b/src/main/kotlin/com/featurevisor/testRunner/Parser.kt similarity index 67% rename from src/main/kotlin/com/featurevisor/cli/Parser.kt rename to src/main/kotlin/com/featurevisor/testRunner/Parser.kt index 9ab57ca..9d4a8d7 100644 --- a/src/main/kotlin/com/featurevisor/cli/Parser.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/Parser.kt @@ -1,4 +1,4 @@ -package com.featurevisor.cli +package com.featurevisor.testRunner import com.featurevisor.sdk.serializers.isValidJson import com.featurevisor.sdk.serializers.mapOperator @@ -8,8 +8,7 @@ import kotlinx.serialization.builtins.serializer import kotlinx.serialization.json.Json import org.yaml.snakeyaml.Yaml import java.io.File -import java.time.format.DateTimeFormatter -import java.util.Date +import java.util.* internal fun parseTestFeatureAssertions(yamlFilePath: String) = try { @@ -21,37 +20,37 @@ internal fun parseTestFeatureAssertions(yamlFilePath: String) = val feature = data["feature"] as? String val segment = data["segment"] as? String - if (!segment.isNullOrEmpty()){ + if (!segment.isNullOrEmpty()) { val segmentAssertion = (data["assertions"] as? List>)!!.map { assertionMap -> - SegmentAssertion( - description = assertionMap["description"] as? String, - context = (assertionMap["context"] as Map).mapValues { parseAttributeValue(it.value) }, - expectedToMatch = assertionMap["expectedToMatch"] as Boolean, - matrix = assertionMap["matrix"] as? AssertionMatrix - ) + SegmentAssertion( + description = assertionMap["description"] as? String, + context = (assertionMap["context"] as Map).mapValues { parseAttributeValue(it.value) }, + expectedToMatch = assertionMap["expectedToMatch"] as Boolean, + matrix = assertionMap["matrix"] as? AssertionMatrix + ) } - val testSegment = TestSegment(key = segment, assertions = segmentAssertion) - Test.Segment(testSegment) - }else if (!feature.isNullOrEmpty()){ + val testSegment = TestSegment(key = segment, assertions = segmentAssertion) + Test.Segment(testSegment) + } else if (!feature.isNullOrEmpty()) { val featureAssertion = (data["assertions"] as? List>)!!.map { assertionMap -> - FeatureAssertion( - description = assertionMap["description"] as? String, - environment = assertionMap["environment"] as String, - at = parseWeightValue((assertionMap["at"] as Any)), - context = (assertionMap["context"] as Map).mapValues { parseAttributeValue(it.value) }, - expectedToBeEnabled = assertionMap["expectedToBeEnabled"] as Boolean, - expectedVariables = (assertionMap["expectedVariables"] as? Map)?.mapValues { - parseVariableValue( - it.value - ) - }, - expectedVariation = assertionMap["expectedVariation"] as? String, - matrix = assertionMap["matrix"] as? AssertionMatrix - ) + FeatureAssertion( + description = assertionMap["description"] as? String, + environment = assertionMap["environment"] as String, + at = parseWeightValue((assertionMap["at"] as Any)), + context = (assertionMap["context"] as Map).mapValues { parseAttributeValue(it.value) }, + expectedToBeEnabled = assertionMap["expectedToBeEnabled"] as Boolean, + expectedVariables = (assertionMap["expectedVariables"] as? Map)?.mapValues { + parseVariableValue( + it.value + ) + }, + expectedVariation = assertionMap["expectedVariation"] as? String, + matrix = assertionMap["matrix"] as? AssertionMatrix + ) } - val testFeature = TestFeature(key = feature, assertions = featureAssertion) - Test.Feature(testFeature) + val testFeature = TestFeature(key = feature, assertions = featureAssertion) + Test.Feature(testFeature) } else { null } @@ -73,47 +72,55 @@ private fun parseVariableValue(value: Any?): VariableValue { return when (value) { is Boolean -> VariableValue.BooleanValue(value) is String -> { - if (isValidJson(value)){ + if (isValidJson(value)) { VariableValue.JsonValue(value) - }else{ + } else { VariableValue.StringValue(value) } } + is Int -> VariableValue.IntValue(value) is Double -> VariableValue.DoubleValue(value) is List<*> -> { val stringList = value.filterIsInstance() VariableValue.ArrayValue(stringList) } + is Map<*, *> -> { val mapData = value as Map - val json1 = Json.encodeToString(MapSerializer(String.serializer(), String.serializer()),mapData) + val json1 = Json.encodeToString(MapSerializer(String.serializer(), String.serializer()), mapData) VariableValue.JsonValue(json1) } + else -> throw IllegalArgumentException("Unsupported variable value type") } } private fun parseAttributeValue(value: Any?): AttributeValue { if (value == null) { - return AttributeValue.StringValue(null) + return AttributeValue.StringValue(null) } return when (value) { is Int -> AttributeValue.IntValue(value) is Double -> AttributeValue.DoubleValue(value) is Boolean -> AttributeValue.BooleanValue(value) -// (value == "") -> AttributeValue.StringValue("") is String -> { - if (value.equals("",true)){ + if (value.equals("", true)) { AttributeValue.StringValue("") - }else{ - AttributeValue.StringValue(value) + } else { + value.toIntOrNull()?.let { + AttributeValue.IntValue(it) + } ?: value.toDoubleOrNull()?.let { + AttributeValue.DoubleValue(it) + } ?: AttributeValue.StringValue(value) } } + is Date -> { AttributeValue.DateValue(value) } + else -> { throw IllegalArgumentException("Unsupported attribute value type") } @@ -166,9 +173,9 @@ private fun parseCondition(conditionData: Any?): Condition { } else -> { - val attributeKey = mapData["attribute"] as AttributeKey - val operatorValue = mapOperator(mapData["operator"] as String) - val value = parseConditionValue(mapData["value"]) + val attributeKey = mapData["attribute"] as AttributeKey + val operatorValue = mapOperator(mapData["operator"] as String) + val value = parseConditionValue(mapData["value"]) Condition.Plain(attributeKey, operatorValue, value) } } @@ -186,12 +193,12 @@ private fun parseCondition(conditionData: Any?): Condition { } -private fun parseConditionValue(value: Any?):ConditionValue{ +private fun parseConditionValue(value: Any?): ConditionValue { if (value == null) { return ConditionValue.StringValue(null) } - return when (value) { + return when (value) { is String -> ConditionValue.StringValue(value) is Int -> ConditionValue.IntValue(value) is Double -> ConditionValue.DoubleValue(value) diff --git a/src/main/kotlin/com/featurevisor/cli/TestExecuter.kt b/src/main/kotlin/com/featurevisor/testRunner/TestExecuter.kt similarity index 99% rename from src/main/kotlin/com/featurevisor/cli/TestExecuter.kt rename to src/main/kotlin/com/featurevisor/testRunner/TestExecuter.kt index 1dd642b..db3d604 100644 --- a/src/main/kotlin/com/featurevisor/cli/TestExecuter.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/TestExecuter.kt @@ -1,6 +1,6 @@ @file:JvmName("TestExecuter") -package com.featurevisor.cli +package com.featurevisor.testRunner import com.featurevisor.types.* import java.io.File @@ -95,7 +95,6 @@ fun testSingleFeature(featureKey: String, projectRootPath: String) { printTestResult(testResult) - if (!testResult.passed) { executionResult.passed = false diff --git a/src/main/kotlin/com/featurevisor/testRunner/TestFeature.kt b/src/main/kotlin/com/featurevisor/testRunner/TestFeature.kt new file mode 100644 index 0000000..1cc0edf --- /dev/null +++ b/src/main/kotlin/com/featurevisor/testRunner/TestFeature.kt @@ -0,0 +1,218 @@ +package com.featurevisor.testRunner + +import com.featurevisor.sdk.getVariable +import com.featurevisor.sdk.getVariation +import com.featurevisor.sdk.isEnabled +import com.featurevisor.types.* +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement + +fun testFeature(testFeature: TestFeature, projectRootPath: String): TestResult { + val featureKey = testFeature.key + + val testResult = TestResult( + type = "feature", + key = featureKey, + notFound = false, + duration = 0, + passed = true, + assertions = mutableListOf() + ) + + testFeature.assertions.forEachIndexed { index, assertion -> + val assertions = getFeatureAssertionsFromMatrix(index, assertion) + + assertions.forEach { + + val testResultAssertion = TestResultAssertion( + description = it.description.orEmpty(), + environment = it.environment, + duration = 0, + passed = true, + errors = mutableListOf() + ) + + + val datafileContent = getDataFileContent( + featureName = testFeature.key, + environment = it.environment, + projectRootPath = projectRootPath + ) + + if (datafileContent != null) { + + val featurevisorInstance = getSdkInstance(datafileContent, it) + + if (testFeature.key.isEmpty()) { + testResult.notFound = true + testResult.passed = false + + return testResult + } + + // isEnabled + if (it.expectedToBeEnabled != null) { + val isEnabled = featurevisorInstance.isEnabled(testFeature.key, it.context) + + if (isEnabled != it.expectedToBeEnabled) { + testResult.passed = false + testResultAssertion.passed = false + + (testResultAssertion.errors as MutableList).add( + TestResultAssertionError( + type = "flag", + expected = it.expectedToBeEnabled, + actual = isEnabled + ) + ) + } + } + + //Variation + if (!it.expectedVariation.isNullOrEmpty()) { + val variation = featurevisorInstance.getVariation(testFeature.key, it.context) + + if (variation != it.expectedVariation) { + testResult.passed = false + testResultAssertion.passed = false + + (testResultAssertion.errors as MutableList).add( + TestResultAssertionError( + type = "variation", + expected = it.expectedVariation, + actual = variation + ) + ) + } + } + + //Variables + if (assertion.expectedVariables is Map<*, *>) { + + assertion.expectedVariables.forEach { (variableKey, expectedValue) -> + val actualValue = featurevisorInstance.getVariable(featureKey, variableKey, it.context) + val passed: Boolean + + val variableSchema = datafileContent.features.find { feature -> + feature.key == testFeature.key + }?.variablesSchema?.find { variableSchema -> + variableSchema.key.equals(variableKey, ignoreCase = true) + } + + if (variableSchema == null) { + testResult.passed = false + testResultAssertion.passed = false + + + (testResultAssertion.errors as MutableList).add( + TestResultAssertionError( + type = "variable", + expected = it.expectedVariation, + actual = null, + message = "schema for variable \"${variableKey}\" not found in feature" + ) + ) + return@forEach + } + + + if (variableSchema.type == VariableType.JSON) { + // JSON type + val parsedExpectedValue = if (expectedValue is VariableValue.StringValue) { + try { + Json.decodeFromString>(expectedValue.value) + } catch (e: Exception) { + expectedValue + } + } else { + expectedValue + } + + passed = when (actualValue) { + is VariableValue.ArrayValue -> checkIfArraysAreEqual( + stringToArray(parsedExpectedValue.toString()).orEmpty().toTypedArray(), + actualValue.values.toTypedArray() + ) + + is VariableValue.ObjectValue -> checkIfObjectsAreEqual( + (parsedExpectedValue as VariableValue.ObjectValue).value, + (actualValue as VariableValue.ObjectValue).value + ) + + is VariableValue.JsonValue -> checkJsonIsEquals( + (expectedValue as VariableValue.JsonValue).value, + (actualValue as VariableValue.JsonValue).value + ) + + else -> parsedExpectedValue == actualValue + } + + if (!passed) { + testResult.passed = false + testResultAssertion.passed = false + + val expectedValueString = + if (expectedValue !is VariableValue.StringValue) expectedValue.toString() else expectedValue + val actualValueString = + if (actualValue !is VariableValue.StringValue) actualValue.toString() else actualValue + + (testResultAssertion.errors as MutableList).add( + TestResultAssertionError( + type = "variable", + expected = expectedValueString, + actual = actualValueString, + details = mapOf("variableKey" to variableKey) + ) + ) + } + } else { + passed = when (expectedValue) { + is VariableValue.ArrayValue -> checkIfArraysAreEqual( + (expectedValue as VariableValue.ArrayValue).values.toTypedArray(), + (actualValue as VariableValue.ArrayValue).values.toTypedArray() + ) + + is VariableValue.ObjectValue -> checkIfObjectsAreEqual(expectedValue, actualValue) + else -> expectedValue == actualValue + } + + if (!passed) { + testResult.passed = false + testResultAssertion.passed = false + + val expectedValueString = + if (expectedValue !is VariableValue.StringValue) expectedValue.toString() else expectedValue + val actualValueString = + if (actualValue !is VariableValue.StringValue) actualValue.toString() else actualValue + + (testResultAssertion.errors as MutableList).add( + TestResultAssertionError( + type = "variable", + expected = expectedValueString, + actual = actualValueString, + details = mapOf("variableKey" to variableKey) + ) + ) + } + } + } + } + } else { + testResult.passed = false + testResultAssertion.passed = false + + (testResultAssertion.errors as MutableList).add( + TestResultAssertionError( + type = "Data File", + expected = null, + actual = null, + message = "Unable to generate Data File" + ) + ) + } + (testResult.assertions as MutableList).add(testResultAssertion) + } + } + return testResult +} diff --git a/src/main/kotlin/com/featurevisor/cli/TestSegment.kt b/src/main/kotlin/com/featurevisor/testRunner/TestSegment.kt similarity index 82% rename from src/main/kotlin/com/featurevisor/cli/TestSegment.kt rename to src/main/kotlin/com/featurevisor/testRunner/TestSegment.kt index c56be38..3ec1dfc 100644 --- a/src/main/kotlin/com/featurevisor/cli/TestSegment.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/TestSegment.kt @@ -1,9 +1,12 @@ -package com.featurevisor.cli +package com.featurevisor.testRunner import com.featurevisor.sdk.segmentIsMatched -import com.featurevisor.types.* +import com.featurevisor.types.TestResult +import com.featurevisor.types.TestResultAssertion +import com.featurevisor.types.TestResultAssertionError +import com.featurevisor.types.TestSegment -fun testSegment(test: TestSegment, segmentFilePath:String):TestResult{ +fun testSegment(test: TestSegment, segmentFilePath: String): TestResult { val segmentKey = test.key val testResult = TestResult( @@ -32,7 +35,7 @@ fun testSegment(test: TestSegment, segmentFilePath:String):TestResult{ val actual = segmentIsMatched(yamlSegment!!, it.context) val passed = actual == expected - if (!passed){ + if (!passed) { val testResultAssertionError = TestResultAssertionError( type = "segment", expected = expected, diff --git a/src/main/kotlin/com/featurevisor/cli/Utils.kt b/src/main/kotlin/com/featurevisor/testRunner/Utils.kt similarity index 89% rename from src/main/kotlin/com/featurevisor/cli/Utils.kt rename to src/main/kotlin/com/featurevisor/testRunner/Utils.kt index 68961c3..434bfc1 100644 --- a/src/main/kotlin/com/featurevisor/cli/Utils.kt +++ b/src/main/kotlin/com/featurevisor/testRunner/Utils.kt @@ -1,4 +1,4 @@ -package com.featurevisor.cli +package com.featurevisor.testRunner import com.featurevisor.sdk.FeaturevisorInstance import com.featurevisor.sdk.InstanceOptions @@ -7,7 +7,7 @@ import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import java.io.File -import java.util.Date +import java.util.* internal const val tick = "\u2713" @@ -24,7 +24,7 @@ internal val json = Json { isLenient = true } -internal fun printMessageInGreenColor(message:String) = +internal fun printMessageInGreenColor(message: String) = println("$ANSI_GREEN$message$ANSI_RESET") internal fun printMessageInRedColor(message: String) = @@ -43,18 +43,18 @@ internal fun getSdkInstance(datafileContent: DatafileContent?, assertion: Featur InstanceOptions( datafile = datafileContent, configureBucketValue = { _, _, _ -> - when(assertion.at){ - is WeightType.IntType -> ((assertion.at as WeightType.IntType).value * (MAX_BUCKETED_NUMBER / 100)) - is WeightType.DoubleType -> ((assertion.at as WeightType.DoubleType).value * (MAX_BUCKETED_NUMBER / 100)).toInt() + when (assertion.at) { + is WeightType.IntType -> ((assertion.at as WeightType.IntType).value * (MAX_BUCKETED_NUMBER / 100)) + is WeightType.DoubleType -> ((assertion.at as WeightType.DoubleType).value * (MAX_BUCKETED_NUMBER / 100)).toInt() else -> (MAX_BUCKETED_NUMBER / 100) } } ) ) - internal fun getFileForSpecificPath(path: String) = File(path) +internal fun getFileForSpecificPath(path: String) = File(path) - internal inline fun String.convertToDataClass() = json.decodeFromString(this) +internal inline fun String.convertToDataClass() = json.decodeFromString(this) internal fun getRootProjectDir(): String { var currentDir = File("").absoluteFile @@ -74,7 +74,7 @@ fun printTestResult(testResult: TestResult) { printNormalMessage("Testing: ${testResult.key}") if (testResult.notFound == true) { - println(ANSI_RED + " => ${testResult.type} ${testResult.key} not found"+ ANSI_RED) + println(ANSI_RED + " => ${testResult.type} ${testResult.key} not found" + ANSI_RED) return } @@ -91,6 +91,7 @@ fun printTestResult(testResult: TestResult) { error.message != null -> { printMessageInRedColor(" => ${error.message}") } + error.type == "variable" -> { val variableKey = (error.details as Map<*, *>)["variableKey"] @@ -98,6 +99,7 @@ fun printTestResult(testResult: TestResult) { printMessageInRedColor(" => expected: ${error.expected}") printMessageInRedColor(" => received: ${error.actual}") } + else -> { printMessageInRedColor( " => ${error.type}: expected \"${error.expected}\", received \"${error.actual}\"" @@ -110,7 +112,7 @@ fun printTestResult(testResult: TestResult) { } fun getContextValue(contextValue: Any?) = - when(contextValue){ + when (contextValue) { is Boolean -> AttributeValue.BooleanValue(contextValue) is Int -> AttributeValue.IntValue(contextValue) is Double -> AttributeValue.DoubleValue(contextValue) @@ -121,16 +123,14 @@ fun getContextValue(contextValue: Any?) = } fun getContextValues(contextValue: AttributeValue) = - when(contextValue){ + when (contextValue) { is AttributeValue.IntValue -> contextValue.value is AttributeValue.DoubleValue -> contextValue.value is AttributeValue.StringValue -> contextValue.value - is AttributeValue.BooleanValue ->contextValue.value + is AttributeValue.BooleanValue -> contextValue.value is AttributeValue.DateValue -> contextValue.value } - - fun checkIfArraysAreEqual(a: Array, b: Array): Boolean { if (a.size != b.size) return false @@ -186,7 +186,7 @@ fun stringToArray(input: String): List? { return null } -fun checkMapIsEquals(a: String, b: String): Boolean { +fun checkJsonIsEquals(a: String, b: String): Boolean { val map1 = Json.decodeFromString>(a) val map2 = Json.decodeFromString>(b) return map1 == map2