From 963044457c8d06d1ffad8e3a599ecf3ed10653de Mon Sep 17 00:00:00 2001 From: Tanmay Ranjan <42682768+Tan108@users.noreply.github.com> Date: Tue, 17 Sep 2024 19:40:40 +0530 Subject: [PATCH 1/4] Test Runner is failing in specific scenarios issue fixes (#51) --- .../kotlin/com/featurevisor/sdk/Conditions.kt | 85 ++++- .../com/featurevisor/sdk/ConditionsTest.kt | 297 ++++++++++++++++-- 2 files changed, 361 insertions(+), 21 deletions(-) diff --git a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt index 41c15dc..bef5b03 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt @@ -42,7 +42,7 @@ object Conditions { EQUALS -> attributeValue.value == conditionValue.value NOT_EQUALS -> attributeValue.value != conditionValue.value CONTAINS -> attributeValue.value?.contains(conditionValue.value.orEmpty()) ?: false - NOT_CONTAINS -> attributeValue.value?.contains(conditionValue.value.orEmpty())?.not() ?: 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( @@ -119,6 +119,65 @@ object Conditions { } } + attributeValue is AttributeValue.IntValue && conditionValue is ConditionValue.ArrayValue -> { + when (operator) { + IN_ARRAY -> attributeValue.value.toString() in conditionValue.values + NOT_IN_ARRAY -> (attributeValue.value.toString() !in conditionValue.values) + else -> false + } + } + + attributeValue is AttributeValue.DoubleValue && conditionValue is ConditionValue.StringValue -> { + when (operator) { + EQUALS -> attributeValue.value.toString() == conditionValue.value + NOT_EQUALS -> attributeValue.value.toString() != conditionValue.value + + SEMVER_EQUALS -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty(), + ) == 0 + + SEMVER_NOT_EQUALS -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty(), + ) != 0 + + SEMVER_GREATER_THAN -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty() + ) == 1 + + SEMVER_GREATER_THAN_OR_EQUALS -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty() + ) >= 0 + + SEMVER_LESS_THAN -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty() + ) == -1 + + SEMVER_LESS_THAN_OR_EQUALS -> compareVersions( + attributeValue.value.toString(), + conditionValue.value.orEmpty() + ) <= 0 + + else -> false + } + } + + attributeValue is AttributeValue.StringValue && conditionValue is ConditionValue.IntValue -> { + when (operator) { + EQUALS -> attributeValue.value == conditionValue.value.toString() + NOT_EQUALS -> attributeValue.value != conditionValue.value.toString() + CONTAINS -> attributeValue.value?.contains(conditionValue.value.toString()) ?: false + NOT_CONTAINS -> attributeValue.value?.contains(conditionValue.value.toString())?.not() ?: false + STARTS_WITH -> attributeValue.value?.startsWith(conditionValue.value.toString()) ?: false + ENDS_WITH -> attributeValue.value?.endsWith(conditionValue.value.toString()) ?: false + else -> false + } + } + attributeValue is AttributeValue.DateValue && conditionValue is ConditionValue.DateTimeValue -> { when (operator) { EQUALS -> attributeValue.value == conditionValue.value @@ -143,9 +202,31 @@ object Conditions { private fun compareVersions(actual: String, condition: String): Int { return try { - SemVer.parse(actual).compareTo(SemVer.parse(condition)) + SemVer.parse(normalizeSemver(actual)).compareTo(SemVer.parse(normalizeSemver(condition))) } catch (e: Exception) { 0 } } + + private fun normalizeSemver(version: String): String { + val parts = version.split("-", "+") + val mainParts = parts[0].split(".").map { it.toInt().toString() } + var normalizedVersion = mainParts.joinToString(".") + + if (version.contains("-")) { + val preRelease = parts[1].split(".").joinToString(".") { + if (it.all { char -> char.isDigit() }) it.toInt().toString() + else it + } + normalizedVersion += "-$preRelease" + } + + if (version.contains("+")) { + val buildMetadata = version.split("+")[1] + normalizedVersion += "+$buildMetadata" + } + + return normalizedVersion + } + } diff --git a/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt b/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt index cc367f4..02feeed 100644 --- a/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt +++ b/src/test/kotlin/com/featurevisor/sdk/ConditionsTest.kt @@ -3,25 +3,8 @@ package com.featurevisor.sdk import com.featurevisor.types.AttributeValue import com.featurevisor.types.Condition import com.featurevisor.types.ConditionValue -import com.featurevisor.types.Operator.AFTER -import com.featurevisor.types.Operator.BEFORE -import com.featurevisor.types.Operator.CONTAINS -import com.featurevisor.types.Operator.ENDS_WITH -import com.featurevisor.types.Operator.EQUALS -import com.featurevisor.types.Operator.GREATER_THAN -import com.featurevisor.types.Operator.GREATER_THAN_OR_EQUALS -import com.featurevisor.types.Operator.IN_ARRAY -import com.featurevisor.types.Operator.LESS_THAN -import com.featurevisor.types.Operator.LESS_THAN_OR_EQUALS -import com.featurevisor.types.Operator.NOT_EQUALS -import com.featurevisor.types.Operator.NOT_IN_ARRAY -import com.featurevisor.types.Operator.SEMVER_EQUALS -import com.featurevisor.types.Operator.SEMVER_GREATER_THAN -import com.featurevisor.types.Operator.SEMVER_GREATER_THAN_OR_EQUALS -import com.featurevisor.types.Operator.SEMVER_LESS_THAN -import com.featurevisor.types.Operator.SEMVER_LESS_THAN_OR_EQUALS -import com.featurevisor.types.Operator.SEMVER_NOT_EQUALS -import com.featurevisor.types.Operator.STARTS_WITH +import com.featurevisor.types.Operator +import com.featurevisor.types.Operator.* import io.kotest.matchers.shouldBe import java.sql.Date import java.time.LocalDate @@ -459,6 +442,282 @@ class ConditionsTest { ) shouldBe true } + @Test + fun `SEMVER_EQUALS operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_EQUALS, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe false + } + + @Test + fun `SEMVER_NOT_EQUALS operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_NOT_EQUALS, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe true + } + + @Test + fun `SEMVER_GREATER_THAN operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_GREATER_THAN, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe true + } + + @Test + fun `SEMVER_GREATER_THAN_OR_EQUALS operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_GREATER_THAN_OR_EQUALS, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.1)) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe true + } + + @Test + fun `SEMVER_LESS_THAN operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_LESS_THAN, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.1)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe false + } + + @Test + fun `SEMVER_LESS_THAN_OR_EQUALS operator works when condition value is String and attribute value is Double`() { + val condition = Condition.Plain( + attributeKey = "version", + operator = SEMVER_LESS_THAN_OR_EQUALS, + value = ConditionValue.StringValue("1.2") + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.1)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.2)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("version" to AttributeValue.DoubleValue(1.3)) + ) shouldBe false + } + + @Test + fun `EQUALS operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = EQUALS, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("1")) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("2")) + ) shouldBe false + } + + @Test + fun `NOT_EQUALS operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = NOT_EQUALS, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("1")) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("2")) + ) shouldBe true + } + + @Test + fun `CONTAINS operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = CONTAINS, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("123")) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("23")) + ) shouldBe false + } + + @Test + fun `NOT_CONTAINS operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = NOT_CONTAINS, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("123")) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("23")) + ) shouldBe true + } + + @Test + fun `STARTS_WITH operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = STARTS_WITH, + value = ConditionValue.IntValue(1) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("123")) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("23")) + ) shouldBe false + } + + @Test + fun `ENDS_WITH operator works when condition value is Int and attribute value is String`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = ENDS_WITH, + value = ConditionValue.IntValue(3) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("123")) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.StringValue("25")) + ) shouldBe false + } + + @Test + fun `IN_ARRAY operator works when condition value is Array and attribute value is Int`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = IN_ARRAY, + value = ConditionValue.ArrayValue(listOf("1","2","3","4")) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.IntValue(1)) + ) shouldBe true + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.IntValue(5)) + ) shouldBe false + } + + @Test + fun `NOT_IN_ARRAY operator works when condition value is Array and attribute value is Int`() { + val condition = Condition.Plain( + attributeKey = "browser_type", + operator = NOT_IN_ARRAY, + value = ConditionValue.ArrayValue(listOf("1","2","3","4")) + ) + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.IntValue(1)) + ) shouldBe false + + Conditions.conditionIsMatched( + condition = condition, + context = mapOf("browser_type" to AttributeValue.IntValue(5)) + ) shouldBe true + } + @Test fun `multiple conditions work`() { val startsWithCondition = Condition.Plain( From 58ff8da7ede680d3bb8537cefc0dec433a704e3c Mon Sep 17 00:00:00 2001 From: Tanmay Ranjan <42682768+Tan108@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:39:17 +0530 Subject: [PATCH 2/4] Fixed : When Semver version contains more than 3 digits then normalisation server throws exception (#54) --- .../kotlin/com/featurevisor/sdk/Conditions.kt | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt index bef5b03..e62453e 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt @@ -210,23 +210,27 @@ object Conditions { private fun normalizeSemver(version: String): String { val parts = version.split("-", "+") - val mainParts = parts[0].split(".").map { it.toInt().toString() } - var normalizedVersion = mainParts.joinToString(".") + val mainParts = parts[0].split(".") - if (version.contains("-")) { + val normalizedMainParts = mainParts.take(3).mapIndexed { _, part -> + val num = part.toIntOrNull() ?: 0 + num.coerceAtMost(999).toString() + } + + var normalizedVersion = normalizedMainParts.joinToString(".") + + if (parts.size > 1 && parts[1].isNotEmpty()) { val preRelease = parts[1].split(".").joinToString(".") { - if (it.all { char -> char.isDigit() }) it.toInt().toString() - else it + it } normalizedVersion += "-$preRelease" } - if (version.contains("+")) { - val buildMetadata = version.split("+")[1] + if (parts.size > 2 && parts[2].isNotEmpty()) { + val buildMetadata = parts[2] normalizedVersion += "+$buildMetadata" } return normalizedVersion } - } From 853346991ea7621db0b3d3cf1f8cb191162d452f Mon Sep 17 00:00:00 2001 From: Tanmay Ranjan <42682768+Tan108@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:39:34 +0530 Subject: [PATCH 3/4] Fix : In array condition is failing when attribute value is null (#52) --- src/main/kotlin/com/featurevisor/sdk/Conditions.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt index e62453e..7f1db80 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt @@ -111,18 +111,19 @@ object Conditions { } } - attributeValue is AttributeValue.StringValue && conditionValue is ConditionValue.ArrayValue -> { + attributeValue is AttributeValue.IntValue && conditionValue is ConditionValue.ArrayValue -> { when (operator) { - IN_ARRAY -> attributeValue.value in conditionValue.values - NOT_IN_ARRAY -> (attributeValue.value !in conditionValue.values) + IN_ARRAY -> attributeValue.value.toString() in conditionValue.values + NOT_IN_ARRAY -> (attributeValue.value.toString() !in conditionValue.values) else -> false } } - attributeValue is AttributeValue.IntValue && conditionValue is ConditionValue.ArrayValue -> { + conditionValue is ConditionValue.ArrayValue -> { + val valueInContext = (context[attributeKey] as? AttributeValue.StringValue)?.value when (operator) { - IN_ARRAY -> attributeValue.value.toString() in conditionValue.values - NOT_IN_ARRAY -> (attributeValue.value.toString() !in conditionValue.values) + IN_ARRAY -> valueInContext in conditionValue.values + NOT_IN_ARRAY -> valueInContext !in conditionValue.values else -> false } } From 47e31397c1227a05c561f2852ed3827419740100 Mon Sep 17 00:00:00 2001 From: Tanmay Ranjan <42682768+Tan108@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:27:47 +0530 Subject: [PATCH 4/4] Revert "Fix : In array condition is failing when attribute value is null" (#55) --- src/main/kotlin/com/featurevisor/sdk/Conditions.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt index 7f1db80..e62453e 100644 --- a/src/main/kotlin/com/featurevisor/sdk/Conditions.kt +++ b/src/main/kotlin/com/featurevisor/sdk/Conditions.kt @@ -111,19 +111,18 @@ object Conditions { } } - attributeValue is AttributeValue.IntValue && conditionValue is ConditionValue.ArrayValue -> { + attributeValue is AttributeValue.StringValue && conditionValue is ConditionValue.ArrayValue -> { when (operator) { - IN_ARRAY -> attributeValue.value.toString() in conditionValue.values - NOT_IN_ARRAY -> (attributeValue.value.toString() !in conditionValue.values) + IN_ARRAY -> attributeValue.value in conditionValue.values + NOT_IN_ARRAY -> (attributeValue.value !in conditionValue.values) else -> false } } - conditionValue is ConditionValue.ArrayValue -> { - val valueInContext = (context[attributeKey] as? AttributeValue.StringValue)?.value + attributeValue is AttributeValue.IntValue && conditionValue is ConditionValue.ArrayValue -> { when (operator) { - IN_ARRAY -> valueInContext in conditionValue.values - NOT_IN_ARRAY -> valueInContext !in conditionValue.values + IN_ARRAY -> attributeValue.value.toString() in conditionValue.values + NOT_IN_ARRAY -> (attributeValue.value.toString() !in conditionValue.values) else -> false } }