Skip to content

Commit

Permalink
Test Runner Implementation (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tan108 authored Mar 8, 2024
1 parent 0fce28f commit 361fe5e
Show file tree
Hide file tree
Showing 14 changed files with 1,277 additions and 103 deletions.
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)
}
}

34 changes: 17 additions & 17 deletions src/main/kotlin/com/featurevisor/sdk/Conditions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,45 +34,45 @@ object Conditions {

fun conditionIsMatched(condition: Plain, context: Context): Boolean {
val (attributeKey, operator, conditionValue) = condition
val attributeValue = context.getOrDefault(attributeKey, null) ?: return false
val attributeValue = context.getOrDefault(attributeKey, null)

return when {
attributeValue is AttributeValue.StringValue && conditionValue is ConditionValue.StringValue -> {
when (operator) {
EQUALS -> attributeValue.value == conditionValue.value
NOT_EQUALS -> attributeValue.value != conditionValue.value
CONTAINS -> attributeValue.value.contains(conditionValue.value)
NOT_CONTAINS -> attributeValue.value.contains(conditionValue.value).not()
STARTS_WITH -> attributeValue.value.startsWith(conditionValue.value)
ENDS_WITH -> attributeValue.value.endsWith(conditionValue.value)
CONTAINS -> attributeValue.value?.contains(conditionValue.value.orEmpty()) ?: false
NOT_CONTAINS -> attributeValue.value?.contains(conditionValue.value.orEmpty())?.not() ?: false
STARTS_WITH -> attributeValue.value?.startsWith(conditionValue.value.orEmpty()) ?: false
ENDS_WITH -> attributeValue.value?.endsWith(conditionValue.value.orEmpty()) ?: false
SEMVER_EQUALS -> compareVersions(
attributeValue.value,
conditionValue.value,
attributeValue.value.orEmpty(),
conditionValue.value.orEmpty(),
) == 0

SEMVER_NOT_EQUALS -> compareVersions(
attributeValue.value,
conditionValue.value,
attributeValue.value.orEmpty(),
conditionValue.value.orEmpty(),
) != 0

SEMVER_GREATER_THAN -> compareVersions(
attributeValue.value,
conditionValue.value
attributeValue.value.orEmpty(),
conditionValue.value.orEmpty()
) == 1

SEMVER_GREATER_THAN_OR_EQUALS -> compareVersions(
attributeValue.value,
conditionValue.value
attributeValue.value.orEmpty(),
conditionValue.value.orEmpty()
) >= 0

SEMVER_LESS_THAN -> compareVersions(
attributeValue.value,
conditionValue.value
attributeValue.value.orEmpty(),
conditionValue.value.orEmpty()
) == -1

SEMVER_LESS_THAN_OR_EQUALS -> compareVersions(
attributeValue.value,
conditionValue.value
attributeValue.value.orEmpty(),
conditionValue.value.orEmpty()
) <= 0

else -> false
Expand Down
36 changes: 25 additions & 11 deletions src/main/kotlin/com/featurevisor/sdk/Instance+Evaluation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -337,7 +341,7 @@ fun FeaturevisorInstance.evaluateFlag(featureKey: FeatureKey, context: Context =
if (feature.ranges.isNullOrEmpty().not()) {

val matchedRange = feature.ranges!!.firstOrNull { range ->
bucketValue >= range.start && bucketValue < range.end
bucketValue >= range.first() && bucketValue < range.last()
}

// matched
Expand Down Expand Up @@ -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,
Expand All @@ -500,6 +506,7 @@ fun FeaturevisorInstance.evaluateVariable(

// bucketing
val bucketValue = getBucketValue(feature, finalContext)

val matchedTrafficAndAllocation = getMatchedTrafficAndAllocation(
traffic = feature.traffic,
context = finalContext,
Expand Down Expand Up @@ -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 ->
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/featurevisor/sdk/Instance+Segments.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal fun FeaturevisorInstance.segmentIsMatched(
return null
}

internal fun FeaturevisorInstance.segmentIsMatched(segment: Segment, context: Context): Boolean {
internal fun segmentIsMatched(segment: Segment, context: Context): Boolean {
return allConditionsAreMatched(segment.conditions, context)
}

Expand Down
43 changes: 33 additions & 10 deletions src/main/kotlin/com/featurevisor/sdk/serializers/Serializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ import com.featurevisor.types.NotGroupSegment
import com.featurevisor.types.Operator
import com.featurevisor.types.OrGroupSegment
import com.featurevisor.types.VariableValue
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.Serializer
import kotlinx.serialization.decodeFromString
import com.featurevisor.types.Required
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.PolymorphicKind
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildSerialDescriptor
Expand All @@ -32,7 +29,29 @@ 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, ExperimentalSerializationApi::class)
@Serializer(forClass = Required::class)
object RequiredSerializer: KSerializer<Required>{
override val descriptor: SerialDescriptor =
buildSerialDescriptor("package.Required", PolymorphicKind.SEALED)

override fun deserialize(decoder: Decoder): Required {
val input = decoder as? JsonDecoder
?: throw SerializationException("This class can be decoded only by Json format")
return when (val tree = input.decodeJsonElement()) {
is JsonPrimitive ->{
Required.FeatureKey(tree.content)
}
is JsonArray -> {
Required.FeatureKey(tree.toString())
}

else -> Required.FeatureKey("abc")
}
}
}

@OptIn(InternalSerializationApi::class)
@Serializer(forClass = Condition::class)
Expand Down Expand Up @@ -247,9 +266,13 @@ object ConditionValueSerializer : KSerializer<ConditionValue> {
} ?: 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)
}
}
}

Expand Down Expand Up @@ -324,7 +347,7 @@ fun isValidJson(jsonString: String): Boolean {
}
}

private fun mapOperator(value: String): Operator {
internal fun mapOperator(value: String): Operator {
return when (value.trim()) {
"equals" -> Operator.EQUALS
"notEquals" -> Operator.NOT_EQUALS
Expand Down
31 changes: 31 additions & 0 deletions src/main/kotlin/com/featurevisor/testRunner/CommandExecuter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.featurevisor.testRunner

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"

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
}
Loading

0 comments on commit 361fe5e

Please sign in to comment.