From ece2ff056cdddb497275f20e558f416993c879de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ronny=20Br=C3=A4unlich?= Date: Fri, 9 Nov 2018 13:10:00 +0100 Subject: [PATCH] Implement two CheckBuilders for performing checks on single and multiple results. Fix bug where the execution after a selection would not continue because a check throws an exception. Add part about CheckBuilders to README. Closes #4 --- README.md | 53 ++++++++++++++++- .../jdbc/action/JdbcSelectAction.scala | 18 ++++-- .../jdbc/check/JdbcAnyCheckBuilder.scala | 46 +++++++++++++++ .../gatling/jdbc/check/JdbcCheckSupport.scala | 8 +++ .../jdbc/SelectAnyCheckSimulation.scala | 57 +++++++++++++++++++ .../jdbc/action/JdbcSelectActionSpec.scala | 13 +++++ .../gatling/jdbc/mock/MockStatsEngine.scala | 8 ++- 7 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 src/main/scala/de/codecentric/gatling/jdbc/check/JdbcAnyCheckBuilder.scala create mode 100644 src/test/scala/de/codecentric/gatling/jdbc/SelectAnyCheckSimulation.scala diff --git a/README.md b/README.md index 50e95a2..5796a9f 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,12 @@ jdbc("drop bar table").drop().table("bar") ### Checks -Currently, checks are only implemented for SELECT. When importing `de.codecentric.gatling.jdbc.Predef._` the `simpleCheck` method is already provided. This method takes a function from `List[Map[String, Any]]` to `Boolean`. +Currently, checks are only implemented for SELECT. When importing `de.codecentric.gatling.jdbc.Predef._` two types of checks are provided. +The first type is the SimpleCheck. + +#### SimpleCheck + +The `simpleCheck` method (importet via `Predef`) allows for very basic checks. This method takes a function from `List[Map[String, Any]]` to `Boolean`. Each element in the list represents a row and the map the individual columns. Checks are simply appended to the selection, e.g.: ```scala exec(jdbc("selection") @@ -136,6 +141,52 @@ exec(jdbc("selection") ``` A SELECT without a WHERE clause can also be validated with a `simpleCheck`. +There is also another type of check that is more closely integrated with Gatling, the `CheckBuilders`. + +#### CheckBuilder + +`CheckBuilder` is actually a class provided by Gatling. Based on the Gatling classes, Gatling JDBC provides two types of them. +The `JdbcAnyCheckBuilder` object contains the instances `SingleAnyResult` and `ManyAnyResults`. Both can be used in the tests quickly by calling either `jdbcSingleResponse` or `jdbcManyResponse`. + +The difference between the two is that the single response extracts the head out of the list of results. So you can only verify a `Map[String, Any]`. +Whereas the many response, like the simple checks, returns a `List[Map[String, Any]]`. Validation is performed via the Gatling API. +E.g. checking a single result can look like this: +```scala +exec(jdbc("selectionSingleCheck") + .select("*") + .from("bar") + .where("abc=4") + .check(jdbcSingleResponse.is(Map[String, Any]("ABC" -> 4, "FOO" -> 4))) +) +``` +This validates the data in the two columns "ABC" and "FOO". Please note explicit typing of the map. Without it the compiler will complain. + +A check with multiple results doesn't look very different: +```scala +exec(jdbc("selectionManyCheck") + .select("*") + .from("bar") + .where("abc=4 OR abc=5") + .check(jdbcManyResponse.is(List( + Map("ABC" -> 4, "FOO" -> 4), + Map("ABC" -> 5, "FOO" -> 5))) + ) +) +``` + +The advantage those CheckBuilder provide is that they can access certain functionality provided by the Gatling interfaces and classes they extend. +The most important one is the possibility to save the result of a selection to the current session. +By calling `saveAs` after a check you can place the result in the session under the given name. So e.g. if you want to store the result of the single check you can do it like this: +```scala +exec(jdbc("selectionSingleCheckSaving") + .select("*") + .from("bar") + .where("abc=4") + .check(jdbcSingleResponse.is(Map[String, Any]("ABC" -> 4, "FOO" -> 4)) + .saveAs("myResult")) +) +``` + ### Final Covering all SQL operations is a lot of work and some special commands might not be required for performance tests. diff --git a/src/main/scala/de/codecentric/gatling/jdbc/action/JdbcSelectAction.scala b/src/main/scala/de/codecentric/gatling/jdbc/action/JdbcSelectAction.scala index 1147229..83fcadc 100644 --- a/src/main/scala/de/codecentric/gatling/jdbc/action/JdbcSelectAction.scala +++ b/src/main/scala/de/codecentric/gatling/jdbc/action/JdbcSelectAction.scala @@ -13,7 +13,7 @@ import scalikejdbc.{DB, SQL} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future -import scala.util.Failure +import scala.util.{Failure, Try} /** * Created by ronny on 11.05.17. @@ -46,13 +46,19 @@ case class JdbcSelectAction(requestName: Expression[String], } } future.onComplete { - case scala.util.Success(value) => performChecks(session, start, value) - case fail: Failure[_] => log(start, nowMillis, fail, requestName, session, statsEngine) + case scala.util.Success(value) => + next ! Try(performChecks(session, start, value)).recover { + case err => + statsEngine.logCrash(session, requestName.apply(session).get, err.getMessage) + session.markAsFailed + }.get + case fail: Failure[_] => + log(start, nowMillis, fail, requestName, session, statsEngine) next ! session } } - private def performChecks(session: Session, start: Long, tried: List[Map[String, Any]]): Unit = { + private def performChecks(session: Session, start: Long, tried: List[Map[String, Any]]): Session = { val (modifySession, error) = Check.check(tried, session, checks) val newSession = modifySession(session) error match { @@ -60,10 +66,10 @@ case class JdbcSelectAction(requestName: Expression[String], requestName.apply(session).map { resolvedRequestName => statsEngine.logResponse(session, resolvedRequestName, ResponseTimings(start, nowMillis), KO, None, None) } - next ! newSession.markAsFailed + newSession.markAsFailed case _ => log(start, nowMillis, scala.util.Success(""), requestName, session, statsEngine) - next ! newSession + newSession } } } diff --git a/src/main/scala/de/codecentric/gatling/jdbc/check/JdbcAnyCheckBuilder.scala b/src/main/scala/de/codecentric/gatling/jdbc/check/JdbcAnyCheckBuilder.scala new file mode 100644 index 0000000..f842eec --- /dev/null +++ b/src/main/scala/de/codecentric/gatling/jdbc/check/JdbcAnyCheckBuilder.scala @@ -0,0 +1,46 @@ +package de.codecentric.gatling.jdbc.check + +import de.codecentric.gatling.jdbc.JdbcCheck +import io.gatling.commons.validation.{Validation, _} +import io.gatling.core.check.extractor.{Extractor, FindAllArity, SingleArity} +import io.gatling.core.check.{DefaultFindCheckBuilder, Extender, Preparer} +import io.gatling.core.session._ + +object JdbcAnyCheckBuilder { + + type ManyAnyResult = List[Map[String, Any]] + + val ManyAnyExtractor: Expression[Extractor[ManyAnyResult, ManyAnyResult] with FindAllArity] = + new Extractor[ManyAnyResult, ManyAnyResult] with FindAllArity { + override def name: String = "manyAny" + + override def apply(prepared: ManyAnyResult): Validation[Option[ManyAnyResult]] = Some(prepared).success + }.expressionSuccess + + val ManyAnyExtender: Extender[JdbcCheck, ManyAnyResult] = check => check + + val ManyAnyPreparer: Preparer[ManyAnyResult, ManyAnyResult] = something => something.success + + val ManyAnyResults = new DefaultFindCheckBuilder[JdbcCheck, ManyAnyResult, ManyAnyResult, ManyAnyResult]( + ManyAnyExtender, + ManyAnyPreparer, + ManyAnyExtractor + ) + + val SingleAnyExtractor: Expression[Extractor[Map[String, Any], Map[String, Any]] with SingleArity] = + new Extractor[Map[String, Any], Map[String, Any]] with SingleArity { + override def name: String = "singleAny" + + override def apply(prepared: Map[String, Any]): Validation[Option[Map[String, Any]]] = Some(prepared).success + }.expressionSuccess + + val SingleAnyExtender: Extender[JdbcCheck, ManyAnyResult] = check => check + + val SingleAnyPreparer: Preparer[ManyAnyResult, Map[String, Any]] = something => something.head.success + + val SingleAnyResult = new DefaultFindCheckBuilder[JdbcCheck, ManyAnyResult, Map[String, Any], Map[String, Any]]( + SingleAnyExtender, + SingleAnyPreparer, + SingleAnyExtractor + ) +} diff --git a/src/main/scala/de/codecentric/gatling/jdbc/check/JdbcCheckSupport.scala b/src/main/scala/de/codecentric/gatling/jdbc/check/JdbcCheckSupport.scala index 2c1dc29..2c2274e 100644 --- a/src/main/scala/de/codecentric/gatling/jdbc/check/JdbcCheckSupport.scala +++ b/src/main/scala/de/codecentric/gatling/jdbc/check/JdbcCheckSupport.scala @@ -1,9 +1,17 @@ package de.codecentric.gatling.jdbc.check +import de.codecentric.gatling.jdbc.JdbcCheck +import de.codecentric.gatling.jdbc.check.JdbcAnyCheckBuilder.ManyAnyResult +import io.gatling.core.check.DefaultFindCheckBuilder + /** * Created by ronny on 15.05.17. */ trait JdbcCheckSupport { def simpleCheck = JdbcSimpleCheck + + val jdbcSingleResponse: DefaultFindCheckBuilder[JdbcCheck, ManyAnyResult, Map[String, Any], Map[String, Any]] = JdbcAnyCheckBuilder.SingleAnyResult + + val jdbcManyResponse: DefaultFindCheckBuilder[JdbcCheck, ManyAnyResult, ManyAnyResult, ManyAnyResult] = JdbcAnyCheckBuilder.ManyAnyResults } diff --git a/src/test/scala/de/codecentric/gatling/jdbc/SelectAnyCheckSimulation.scala b/src/test/scala/de/codecentric/gatling/jdbc/SelectAnyCheckSimulation.scala new file mode 100644 index 0000000..e20ff58 --- /dev/null +++ b/src/test/scala/de/codecentric/gatling/jdbc/SelectAnyCheckSimulation.scala @@ -0,0 +1,57 @@ +package de.codecentric.gatling.jdbc + +import de.codecentric.gatling.jdbc.Predef._ +import de.codecentric.gatling.jdbc.builder.column.ColumnHelper._ +import io.gatling.core.Predef._ +import io.gatling.core.scenario.Simulation + +/** + * Created by ronny on 10.05.17. + */ +class SelectAnyCheckSimulation extends Simulation { + + val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") + + val testScenario = scenario("createTable"). + exec(jdbc("bar table") + .create() + .table("bar") + .columns( + column( + name("abc"), + dataType("INTEGER"), + constraint("PRIMARY KEY") + ), + column( + name("foo"), + dataType("INTEGER") + ) + ) + ).repeat(10, "n") { + exec(jdbc("insertion") + .insert() + .into("bar") + .values("${n}, ${n}") + ) + }.pause(1). + exec(jdbc("selectionSingleCheck") + .select("*") + .from("bar") + .where("abc=4") + .check(jdbcSingleResponse.is(Map[String, Any]("ABC" -> 4, "FOO" -> 4)) + .saveAs("myResult")) + ).pause(1). + exec(jdbc("selectionManyCheck") + .select("*") + .from("bar") + .where("abc=4 OR abc=5") + .check(jdbcManyResponse.is(List( + Map("ABC" -> 4, "FOO" -> 4), + Map("ABC" -> 5, "FOO" -> 5))) + ) + ) + //.exec(session => session("something").as[List[Map[String, Any]]]) + + + setUp(testScenario.inject(atOnceUsers(1))).protocols(jdbcConfig) +} diff --git a/src/test/scala/de/codecentric/gatling/jdbc/action/JdbcSelectActionSpec.scala b/src/test/scala/de/codecentric/gatling/jdbc/action/JdbcSelectActionSpec.scala index 57ad471..21b5c87 100644 --- a/src/test/scala/de/codecentric/gatling/jdbc/action/JdbcSelectActionSpec.scala +++ b/src/test/scala/de/codecentric/gatling/jdbc/action/JdbcSelectActionSpec.scala @@ -134,4 +134,17 @@ class JdbcSelectActionSpec extends JdbcActionSpec { waitForLatch(nextAction) nextAction.called should be(true) } + + it should "pass the session to the next action even when a check crashes" in { + DB autoCommit { implicit session => + sql"""CREATE TABLE crashes(id INTEGER PRIMARY KEY )""".execute().apply() + } + val nextAction = NextAction(session.markAsFailed) + val action = JdbcSelectAction("request", "*", "CRASHES", None, List(simpleCheck(_ => throw new RuntimeException("Test error"))), statsEngine, nextAction) + + action.execute(session) + + waitForLatch(nextAction) + nextAction.called should be(true) + } } diff --git a/src/test/scala/de/codecentric/gatling/jdbc/mock/MockStatsEngine.scala b/src/test/scala/de/codecentric/gatling/jdbc/mock/MockStatsEngine.scala index 7a30061..8692d7a 100644 --- a/src/test/scala/de/codecentric/gatling/jdbc/mock/MockStatsEngine.scala +++ b/src/test/scala/de/codecentric/gatling/jdbc/mock/MockStatsEngine.scala @@ -15,12 +15,13 @@ */ package de.codecentric.gatling.jdbc.mock +import java.util.Date + import io.gatling.commons.stats.Status import io.gatling.core.session.{GroupBlock, Session} import io.gatling.core.stats.StatsEngine import io.gatling.core.stats.message.ResponseTimings -import io.gatling.core.stats.writer.{DataWriterMessage, GroupMessage, ResponseMessage, UserMessage} - +import io.gatling.core.stats.writer._ import akka.actor.ActorRef import com.typesafe.scalalogging.StrictLogging @@ -58,7 +59,8 @@ class MockStatsEngine extends StatsEngine with StrictLogging { override def logGroupEnd(session: Session, group: GroupBlock, exitTimestamp: Long): Unit = handle(GroupMessage(session.scenario, session.userId, group.hierarchy, group.startTimestamp, exitTimestamp, group.cumulatedResponseTime, group.status)) - override def logCrash(session: Session, requestName: String, error: String): Unit = {} + override def logCrash(session: Session, requestName: String, error: String): Unit = + handle(ErrorMessage(error, new Date().getTime)) override def reportUnbuildableRequest(session: Session, requestName: String, errorMessage: String): Unit = {}