Skip to content

Commit

Permalink
Implement two CheckBuilders for performing checks on single and multi…
Browse files Browse the repository at this point in the history
…ple 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
  • Loading branch information
Ronny Bräunlich committed Nov 9, 2018
1 parent df5df1d commit ece2ff0
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 10 deletions.
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -46,24 +46,30 @@ 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 {
case Some(failure) =>
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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
)
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 = {}

Expand Down

0 comments on commit ece2ff0

Please sign in to comment.