Compare commits
4 Commits
3fa39bb4f4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 25ecc1e097 | |||
| 427ed920d0 | |||
| 99ea686ed5 | |||
| 0db2bb777f |
+13
-1
@@ -19,7 +19,19 @@ tasks.withType(io.gatling.gradle.GatlingRunTask) {
|
||||
jvmArgs = [
|
||||
'--add-opens=java.base/java.lang=ALL-UNNAMED',
|
||||
"-Dtarget.baseUrl=${findProperty('baseUrl') ?: 'http://localhost:8080'}",
|
||||
"-Dgatling.authToken=${findProperty('authToken') ?: System.getenv('GATLING_AUTH_TOKEN') ?: ''}"
|
||||
"-Dgatling.authToken=${findProperty('authToken') ?: System.getenv('GATLING_AUTH_TOKEN') ?: ''}",
|
||||
"-Dhealthz.path=${findProperty('healthzPath') ?: '/health'}",
|
||||
"-DmaxUsers=${findProperty('maxUsers') ?: '10'}",
|
||||
"-DrampDuration=${findProperty('rampDuration') ?: '60'}",
|
||||
"-DstartUsers=${findProperty('startUsers') ?: '2'}",
|
||||
"-DusersIncrement=${findProperty('usersIncrement') ?: '2'}",
|
||||
"-Dsteps=${findProperty('steps') ?: '2'}",
|
||||
"-DstepDuration=${findProperty('stepDuration') ?: '30'}",
|
||||
"-DconcurrentUsers=${findProperty('concurrentUsers') ?: '3'}",
|
||||
"-Dduration=${findProperty('duration') ?: '300'}",
|
||||
"-DbaselineUsers=${findProperty('baselineUsers') ?: '2'}",
|
||||
"-DbaselineDuration=${findProperty('baselineDuration') ?: '20'}",
|
||||
"-DspikeUsers=${findProperty('spikeUsers') ?: '15'}"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
package base
|
||||
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.core.structure.ScenarioBuilder
|
||||
import io.gatling.http.Predef._
|
||||
import io.gatling.http.protocol.HttpProtocolBuilder
|
||||
|
||||
/**
|
||||
* Base class for all simulations.
|
||||
*
|
||||
* Reads runtime configuration from JVM system properties set by Gradle:
|
||||
* -Dtarget.baseUrl=... (via -PbaseUrl=... on the Gradle command line)
|
||||
* -Dgatling.authToken=... (via -PauthToken=... or env GATLING_AUTH_TOKEN)
|
||||
*/
|
||||
abstract class BaseSimulation extends Simulation {
|
||||
|
||||
protected val baseUrl: String =
|
||||
@@ -23,4 +17,21 @@ abstract class BaseSimulation extends Simulation {
|
||||
.baseUrl(baseUrl)
|
||||
.header("Authorization", s"Bearer $authToken")
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
protected def scenarioFromEndpoint(endpoint: base.Endpoint): ScenarioBuilder = {
|
||||
val base = endpoint.method.toUpperCase match {
|
||||
case "GET" => http(endpoint.name).get(endpoint.path)
|
||||
case "POST" => http(endpoint.name).post(endpoint.path)
|
||||
case "PUT" => http(endpoint.name).put(endpoint.path)
|
||||
case "DELETE" => http(endpoint.name).delete(endpoint.path)
|
||||
case "PATCH" => http(endpoint.name).patch(endpoint.path)
|
||||
}
|
||||
val withBody = endpoint.body.fold(base)(b => base.body(StringBody(b)))
|
||||
scenario(endpoint.name).exec(
|
||||
withBody
|
||||
.check(status.is(endpoint.expectedStatus))
|
||||
.check(responseTimeInMillis.lte(endpoint.maxResponseTimeMs))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package base
|
||||
|
||||
case class Endpoint(
|
||||
name: String,
|
||||
method: String,
|
||||
path: String,
|
||||
body: Option[String] = None,
|
||||
expectedStatus: Int = 200,
|
||||
maxResponseTimeMs: Int = 5000
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package endpoints
|
||||
|
||||
import base.Endpoint
|
||||
|
||||
object BoardEndpoints {
|
||||
|
||||
val createGame: Endpoint = Endpoint(
|
||||
name = "Create Game",
|
||||
method = "GET",
|
||||
path = "/api/account/official-bots/",
|
||||
// expectedStatus = 200
|
||||
)
|
||||
|
||||
val all: List[Endpoint] = List(createGame)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package scenarios
|
||||
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.core.structure.ScenarioBuilder
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
object ChessUserScenario {
|
||||
|
||||
private def makeMove(uci: String) =
|
||||
http(s"Move $uci")
|
||||
.post(session => s"/api/board/game/${session("gameId").as[String]}/move/$uci")
|
||||
.header("Authorization", "${jwt}")
|
||||
.check(status.in(200, 201))
|
||||
|
||||
val play: ScenarioBuilder = scenario("Chess User Journey")
|
||||
.exec(session => session.set("username", s"user_${System.currentTimeMillis()}_${session.userId}_${java.util.UUID.randomUUID().toString.take(8)}"))
|
||||
.exec(
|
||||
http("Register")
|
||||
.post("/api/account")
|
||||
.body(StringBody(session =>
|
||||
s"""{"username":"${session("username").as[String]}","email":"${session("username").as[String]}@test.com","password":"Password123!"}"""
|
||||
))
|
||||
.check(status.is(200))
|
||||
)
|
||||
.exec(
|
||||
http("Login")
|
||||
.post("/api/account/login")
|
||||
.body(StringBody(session =>
|
||||
s"""{"username":"${session("username").as[String]}","password":"Password123!"}"""
|
||||
))
|
||||
.check(status.is(200))
|
||||
.check(jsonPath("$.token").saveAs("jwt"))
|
||||
)
|
||||
.exec(
|
||||
http("Import Game")
|
||||
.post("/api/board/game/import/fen")
|
||||
.header("Authorization", "${jwt}")
|
||||
.body(StringBody(session => {
|
||||
val username = session("username").as[String]
|
||||
s"""{
|
||||
| "fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
||||
| "white": {"id": "$username", "displayName": "$username"},
|
||||
| "black": {"id": "opponent_${session.userId}", "displayName": "Opponent"},
|
||||
| "timeControl": {"limitSeconds": 300, "incrementSeconds": 3}
|
||||
|}""".stripMargin
|
||||
}))
|
||||
.check(status.in(200, 201))
|
||||
.check(jsonPath("$.gameId").saveAs("gameId"))
|
||||
)
|
||||
.exec(makeMove("e2e4"))
|
||||
.exec(makeMove("e7e5"))
|
||||
.exec(makeMove("g1f3"))
|
||||
.exec(
|
||||
http("Resign")
|
||||
.post(session => s"/api/board/game/${session("gameId").as[String]}/resign")
|
||||
.header("Authorization", "${jwt}")
|
||||
.check(status.in(200, 201, 204))
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package simulations
|
||||
|
||||
import base.BaseSimulation
|
||||
import scenarios.ChessUserScenario
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class EnduranceTestSimulation extends BaseSimulation {
|
||||
|
||||
private val concurrentUsers = sys.props.getOrElse("concurrentUsers", "3").toInt
|
||||
private val duration = sys.props.getOrElse("duration", "300").toInt
|
||||
|
||||
override protected val httpProtocol = http
|
||||
.baseUrl(baseUrl)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
setUp(
|
||||
ChessUserScenario.play
|
||||
.inject(
|
||||
constantConcurrentUsers(concurrentUsers).during(duration.seconds)
|
||||
)
|
||||
).protocols(httpProtocol)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package simulations
|
||||
|
||||
import base.BaseSimulation
|
||||
import scenarios.ChessUserScenario
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class LoadTestSimulation extends BaseSimulation {
|
||||
|
||||
private val maxUsers = sys.props.getOrElse("maxUsers", "5").toInt
|
||||
private val rampDuration = sys.props.getOrElse("rampDuration", "60").toInt
|
||||
|
||||
// Each virtual user authenticates individually, so no global Bearer token
|
||||
override protected val httpProtocol = http
|
||||
.baseUrl(baseUrl)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
setUp(
|
||||
ChessUserScenario.play
|
||||
.inject(rampUsers(maxUsers).during(rampDuration.seconds))
|
||||
).protocols(httpProtocol)
|
||||
}
|
||||
@@ -1,35 +1,19 @@
|
||||
package simulations
|
||||
|
||||
import base.BaseSimulation
|
||||
import scenarios.ChessUserScenario
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
/**
|
||||
* Smoke test: verifies the cluster is reachable and the bearer token is accepted.
|
||||
*
|
||||
* Runs a single virtual user that hits GET /healthz and asserts:
|
||||
* - HTTP 200
|
||||
* - Response time under 2 000 ms
|
||||
*
|
||||
* Use this as a reference when writing new simulations.
|
||||
*
|
||||
* Run:
|
||||
* ./gradlew gatlingRun -PbaseUrl=http://<cluster-service> -PauthToken=<token>
|
||||
*/
|
||||
class SmokeTestSimulation extends BaseSimulation {
|
||||
|
||||
private val healthzPath: String =
|
||||
sys.props.getOrElse("healthz.path", "/healthz")
|
||||
|
||||
private val scn = scenario("Smoke Test")
|
||||
.exec(
|
||||
http(s"GET $healthzPath")
|
||||
.get(healthzPath)
|
||||
.check(status.is(200))
|
||||
.check(responseTimeInMillis.lte(2000))
|
||||
)
|
||||
override protected val httpProtocol = http
|
||||
.baseUrl(baseUrl)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
setUp(
|
||||
scn.inject(atOnceUsers(1))
|
||||
ChessUserScenario.play
|
||||
.inject(atOnceUsers(1))
|
||||
).protocols(httpProtocol)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package simulations
|
||||
|
||||
import base.BaseSimulation
|
||||
import scenarios.ChessUserScenario
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class SpikeTestSimulation extends BaseSimulation {
|
||||
|
||||
private val baselineUsers = sys.props.getOrElse("baselineUsers", "2").toInt
|
||||
private val baselineDuration = sys.props.getOrElse("baselineDuration", "20").toInt
|
||||
private val spikeUsers = sys.props.getOrElse("spikeUsers", "15").toInt
|
||||
|
||||
override protected val httpProtocol = http
|
||||
.baseUrl(baseUrl)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
setUp(
|
||||
ChessUserScenario.play
|
||||
.inject(
|
||||
constantUsersPerSec(baselineUsers).during(baselineDuration.seconds),
|
||||
atOnceUsers(spikeUsers),
|
||||
nothingFor(5.seconds),
|
||||
constantUsersPerSec(baselineUsers).during(baselineDuration.seconds)
|
||||
)
|
||||
).protocols(httpProtocol)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package simulations
|
||||
|
||||
import base.BaseSimulation
|
||||
import scenarios.ChessUserScenario
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class StressTestSimulation extends BaseSimulation {
|
||||
|
||||
private val startUsers = sys.props.getOrElse("startUsers", "2").toInt
|
||||
private val usersIncrement = sys.props.getOrElse("usersIncrement", "2").toInt
|
||||
private val steps = sys.props.getOrElse("steps", "2").toInt
|
||||
private val stepDuration = sys.props.getOrElse("stepDuration", "30").toInt
|
||||
private val rampDuration = sys.props.getOrElse("rampDuration", "10").toInt
|
||||
|
||||
override protected val httpProtocol = http
|
||||
.baseUrl(baseUrl)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
setUp(
|
||||
ChessUserScenario.play
|
||||
.inject(
|
||||
incrementConcurrentUsers(usersIncrement)
|
||||
.times(steps)
|
||||
.eachLevelLasting(stepDuration.seconds)
|
||||
.separatedByRampsLasting(rampDuration.seconds)
|
||||
.startingFrom(startUsers)
|
||||
)
|
||||
).protocols(httpProtocol)
|
||||
}
|
||||
Reference in New Issue
Block a user