Skip to content

Commit

Permalink
Handle basic concurrency in the StubCache data structure
Browse files Browse the repository at this point in the history
  • Loading branch information
yogeshnikam671 committed Nov 18, 2024
1 parent a1869a6 commit 0264da9
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 7 deletions.
15 changes: 10 additions & 5 deletions core/src/main/kotlin/io/specmatic/stub/stateful/StubCache.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package io.specmatic.stub.stateful

import io.specmatic.core.value.JSONArrayValue
import io.specmatic.core.value.JSONObjectValue
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

data class CachedResponse(
val path: String,
Expand All @@ -10,8 +12,9 @@ data class CachedResponse(

class StubCache {
private val cachedResponses = mutableListOf<CachedResponse>()
private val lock = ReentrantLock()

fun addResponse(path: String, responseBody: JSONObjectValue) {
fun addResponse(path: String, responseBody: JSONObjectValue) = lock.withLock {
cachedResponses.add(
CachedResponse(path, responseBody)
)
Expand All @@ -22,12 +25,12 @@ class StubCache {
responseBody: JSONObjectValue,
idKey: String,
idValue: String
) {
) = lock.withLock {
deleteResponse(path, idKey, idValue)
addResponse(path, responseBody)
}

fun findResponseFor(path: String, idKey: String, idValue: String): CachedResponse? {
fun findResponseFor(path: String, idKey: String, idValue: String): CachedResponse? = lock.withLock {
return cachedResponses.filter {
it.path == path
}.firstOrNull {
Expand All @@ -36,7 +39,7 @@ class StubCache {
}
}

fun findAllResponsesFor(path: String, attributeSelectionKeys: Set<String>): JSONArrayValue {
fun findAllResponsesFor(path: String, attributeSelectionKeys: Set<String>): JSONArrayValue = lock.withLock {
val responseBodies = cachedResponses.filter { it.path == path }.map {
it.responseBody.removeKeysNotPresentIn(attributeSelectionKeys)
}
Expand All @@ -45,6 +48,8 @@ class StubCache {

fun deleteResponse(path: String, idKey: String, idValue: String) {
val existingResponse = findResponseFor(path, idKey, idValue) ?: return
cachedResponses.remove(existingResponse)
lock.withLock {
cachedResponses.remove(existingResponse)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import io.specmatic.core.utilities.ContractPathData
import io.specmatic.core.value.*
import io.specmatic.stub.ContractStub
import io.specmatic.stub.loadContractStubsFromImplicitPaths
import io.specmatic.stub.stateful.StatefulHttpStubTest.Companion
import io.specmatic.stub.stateful.StatefulHttpStubTest.Companion.resourceId
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.MethodOrderer
import org.junit.jupiter.api.Order
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestMethodOrder
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors

@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
class StatefulHttpStubTest {
Expand Down Expand Up @@ -401,6 +401,87 @@ class StatefulHttpStubSeedDataFromExamplesTest {

}

class StatefulHttpStubConcurrencyTest {
companion object {
private lateinit var httpStub: ContractStub
private const val SPEC_DIR_PATH = "src/test/resources/openapi/spec_with_strictly_restful_apis"
private var resourceId = ""

@JvmStatic
@BeforeAll
fun setup() {
httpStub = StatefulHttpStub(
specmaticConfigPath = "$SPEC_DIR_PATH/specmatic.yaml",
features = listOf(
OpenApiSpecification.fromFile(
"$SPEC_DIR_PATH/spec_with_strictly_restful_apis.yaml"
).toFeature()
)
)
}

@JvmStatic
@AfterAll
fun tearDown() {
httpStub.close()
}
}

@Test
fun `should handle concurrent additions and updates without corruption`() {
val numberOfThreads = 10
val executor = Executors.newFixedThreadPool(numberOfThreads)
val latch = CountDownLatch(numberOfThreads)

repeat(numberOfThreads) { threadIndex ->
executor.submit {
try {
val path = "/products"

httpStub.client.execute(
HttpRequest(
method = "POST",
path = path,
body = parsedJSONObject(
"""
{
"name": "Product $threadIndex",
"price": ${threadIndex * 10}
}
""".trimIndent()
)
)
)
} finally {
latch.countDown()
}
}
}

latch.await()
executor.shutdown()

// Verify all products were added
val response = httpStub.client.execute(
HttpRequest(
method = "GET",
path = "/products"
)
)

assertThat(response.status).isEqualTo(200)
assertThat(response.body).isInstanceOf(JSONArrayValue::class.java)

val products = (response.body as JSONArrayValue).list
assertThat(products.size).isEqualTo(numberOfThreads)
products.sortedBy { (it as JSONObjectValue).getStringValue("name") }.forEachIndexed { index, product ->
val productObject = product as JSONObjectValue
assertThat(productObject.getStringValue("name")).isEqualTo("Product $index")
assertThat(productObject.getStringValue("price")).isEqualTo("${index * 10}")
}
}
}

private fun JSONObjectValue.getStringValue(key: String): String? {
return this.jsonObject[key]?.toStringLiteral()
}

0 comments on commit 0264da9

Please sign in to comment.