├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── src ├── test │ ├── resources │ │ └── docker │ │ │ ├── mysql │ │ │ └── startMySql.sh │ │ │ └── postgres │ │ │ └── startPostges.sh │ └── scala │ │ └── dev │ │ └── code_n_roll │ │ └── gatling │ │ └── jdbc │ │ ├── action │ │ ├── BlockingLatchAction.scala │ │ ├── JdbcActionSpec.scala │ │ ├── JdbcDropTableActionSpec.scala │ │ ├── JdbcInsertActionSpec.scala │ │ ├── JdbcDeletionActionSpec.scala │ │ ├── JdbcCreateTableActionSpec.scala │ │ └── JdbcSelectActionSpec.scala │ │ ├── protocol │ │ └── JdbcProtocolSpec.scala │ │ ├── simulation │ │ ├── CreateTableSimulation.scala │ │ ├── DropTableSimulation.scala │ │ ├── SelectSimulation.scala │ │ ├── DeleteSimulation.scala │ │ ├── SelectCheckSimulation.scala │ │ ├── GroupInsertSimulation.scala │ │ ├── InsertSimulation.scala │ │ ├── InsertMySqlSimulation.scala │ │ ├── InsertPostgresSimulation.scala │ │ └── SelectTypedCheckSimulation.scala │ │ ├── check │ │ └── JdbcSimpleCheckSpec.scala │ │ └── mock │ │ └── MockStatsEngine.scala └── main │ └── scala │ └── dev │ └── code_n_roll │ └── gatling │ └── jdbc │ ├── Predef.scala │ ├── package.scala │ ├── builder │ ├── JdbcTableDropBuilderBase.scala │ ├── JdbcDeletionBuilderBase.scala │ ├── JdbcSelectionBuilderBase.scala │ ├── JdbcActionBuilderBase.scala │ ├── JdbcTableCreationBuilderBase.scala │ ├── column │ │ └── ColumnHelper.scala │ └── JdbcInsertionBuilderBase.scala │ ├── protocol │ ├── JdbcComponents.scala │ ├── JdbcProtocolBuilderBase.scala │ └── JdbcProtocol.scala │ ├── check │ ├── JdbcCheckActionBuilder.scala │ ├── JdbcSimpleCheck.scala │ ├── JdbcSingleTCheck.scala │ ├── JdbcManyTCheck.scala │ └── JdbcCheckSupport.scala │ ├── action │ ├── JdbcTableDroppingActionBuilder.scala │ ├── JdbcInsertionActionBuilder.scala │ ├── JdbcTableCreationActionBuilder.scala │ ├── JdbcAction.scala │ ├── JdbcDeletionActionBuilder.scala │ ├── JdbcDropTableAction.scala │ ├── JdbcInsertAction.scala │ ├── JdbcDeletionAction.scala │ ├── JdbcCreateTableAction.scala │ ├── JdbcSelectionActionBuilder.scala │ └── JdbcSelectAction.scala │ └── JdbcDsl.scala ├── .travis.yml ├── .gitignore ├── README.md └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.1.5 -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "2.3.1-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /src/test/resources/docker/mysql/startMySql.sh: -------------------------------------------------------------------------------- 1 | docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7 -------------------------------------------------------------------------------- /src/test/resources/docker/postgres/startPostges.sh: -------------------------------------------------------------------------------- 1 | docker run --name some-postgres -p 5432:5432 -e POSTGRES_PASSWORD=mysecretpassword -d postgres:9.6 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: scala 4 | scala: 5 | - 2.12.8 6 | jdk: 7 | - oraclejdk8 8 | services: 9 | - docker 10 | script: 11 | sbt ++$TRAVIS_SCALA_VERSION test && sbt ++$TRAVIS_SCALA_VERSION gatling:test -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/Predef.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc 2 | 3 | /** 4 | * Created by ronny on 10.05.17. 5 | */ 6 | object Predef extends JdbcDsl { 7 | 8 | @Deprecated 9 | type ManyAnyResult = List[Map[String, Any]] 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/package.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling 2 | 3 | import io.gatling.core.check.Check 4 | 5 | /** 6 | * Created by ronny on 15.05.17. 7 | */ 8 | package object jdbc { 9 | 10 | type JdbcCheck[T] = Check[List[T]] 11 | 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | 19 | .idea 20 | *.iml 21 | results/ -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.gatling" % "gatling-sbt" % "3.0.0") 2 | // for releasing 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") 4 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") 5 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.4") 6 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.0") 7 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.9") 8 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") 9 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/builder/JdbcTableDropBuilderBase.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.builder 2 | 3 | import dev.code_n_roll.gatling.jdbc.action.JdbcTableDroppingActionBuilder 4 | import io.gatling.core.session.Expression 5 | 6 | /** 7 | * Created by ronny on 11.05.17. 8 | */ 9 | case class JdbcTableDropBuilderBase(requestName: Expression[String]) { 10 | 11 | def table(name: Expression[String]) = JdbcTableDroppingActionBuilder(requestName, name) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/builder/JdbcDeletionBuilderBase.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.builder 2 | 3 | import dev.code_n_roll.gatling.jdbc.action.JdbcDeletionWithoutWhereActionBuilder 4 | import io.gatling.core.session.Expression 5 | 6 | /** 7 | * Created by ronny on 11.05.17. 8 | */ 9 | case class JdbcDeletionBuilderBase(requestName: Expression[String]) { 10 | 11 | def from(tableName: Expression[String]) = JdbcDeletionWithoutWhereActionBuilder(requestName, tableName) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/protocol/JdbcComponents.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.protocol 2 | 3 | import io.gatling.core.protocol.ProtocolComponents 4 | import io.gatling.core.session.Session 5 | 6 | /** 7 | * Created by ronny on 10.05.17. 8 | */ 9 | case class JdbcComponents(protocol: JdbcProtocol) extends ProtocolComponents { 10 | 11 | override def onStart: Session => Session = ProtocolComponents.NoopOnStart 12 | 13 | override def onExit: Session => Unit = ProtocolComponents.NoopOnExit 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/builder/JdbcSelectionBuilderBase.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.builder 2 | 3 | import dev.code_n_roll.gatling.jdbc.action.JdbcSelectionWithoutWhereActionBuilder 4 | import io.gatling.core.session.Expression 5 | 6 | /** 7 | * Created by ronny on 11.05.17. 8 | */ 9 | case class JdbcSelectionBuilderBase(requestName: Expression[String], what: Expression[String]) { 10 | 11 | def from(from: Expression[String]) = JdbcSelectionWithoutWhereActionBuilder(requestName, what, from) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/check/JdbcCheckActionBuilder.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.check 2 | 3 | import dev.code_n_roll.gatling.jdbc.JdbcCheck 4 | import io.gatling.core.action.builder.ActionBuilder 5 | 6 | import scala.collection.mutable.ArrayBuffer 7 | 8 | /** 9 | * Created by ronny on 15.05.17. 10 | */ 11 | trait JdbcCheckActionBuilder[T] extends ActionBuilder { 12 | 13 | protected val checks: ArrayBuffer[JdbcCheck[T]] = ArrayBuffer.empty 14 | 15 | def check(check: JdbcCheck[T]): ActionBuilder = { 16 | checks += check 17 | this 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/builder/JdbcActionBuilderBase.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.builder 2 | 3 | import io.gatling.core.config.GatlingConfiguration 4 | import io.gatling.core.session.Expression 5 | 6 | /** 7 | * Created by ronny on 10.05.17. 8 | */ 9 | case class JdbcActionBuilderBase(requestName: Expression[String]) { 10 | 11 | def create() = JdbcTableCreationBuilderBase(requestName) 12 | 13 | def insert() = JdbcInsertionBuilderBase(requestName) 14 | 15 | def select(what: Expression[String]) = JdbcSelectionBuilderBase(requestName, what) 16 | 17 | def drop() = JdbcTableDropBuilderBase(requestName) 18 | 19 | def delete() = JdbcDeletionBuilderBase(requestName) 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcTableDroppingActionBuilder.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.core.action.Action 4 | import io.gatling.core.action.builder.ActionBuilder 5 | import io.gatling.core.session.Expression 6 | import io.gatling.core.structure.ScenarioContext 7 | 8 | /** 9 | * Created by ronny on 11.05.17. 10 | */ 11 | case class JdbcTableDroppingActionBuilder(requestName: Expression[String], tableName: Expression[String]) extends ActionBuilder { 12 | 13 | override def build(ctx: ScenarioContext, next: Action): Action = { 14 | val statsEngine = ctx.coreComponents.statsEngine 15 | val clock = ctx.coreComponents.clock 16 | JdbcDropTableAction(requestName, tableName, clock, statsEngine, next) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/action/BlockingLatchAction.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import java.util.concurrent.CountDownLatch 4 | 5 | import io.gatling.core.action.Action 6 | import io.gatling.core.session.Session 7 | 8 | /** 9 | * This action has a latch with a count 1 on which one can wait in order 10 | * to test async execution. When execute is being called the latch is opened. 11 | */ 12 | class BlockingLatchAction extends Action{ 13 | 14 | val latch: CountDownLatch = new CountDownLatch(1) 15 | 16 | override def name: String = "latch action" 17 | 18 | override def execute(session: Session): Unit = latch.countDown() 19 | } 20 | 21 | object BlockingLatchAction{ 22 | def apply(): BlockingLatchAction = new BlockingLatchAction() 23 | } -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcInsertionActionBuilder.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.core.action.Action 4 | import io.gatling.core.action.builder.ActionBuilder 5 | import io.gatling.core.session.Expression 6 | import io.gatling.core.structure.ScenarioContext 7 | 8 | /** 9 | * Created by ronny on 11.05.17. 10 | */ 11 | case class JdbcInsertionActionBuilder(requestName: Expression[String], tableName: Expression[String], values: Expression[String]) extends ActionBuilder { 12 | 13 | override def build(ctx: ScenarioContext, next: Action): Action = { 14 | val statsEngine = ctx.coreComponents.statsEngine 15 | val clock = ctx.coreComponents.clock 16 | JdbcInsertAction(requestName, tableName, values, clock, statsEngine, next) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/builder/JdbcTableCreationBuilderBase.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.builder 2 | 3 | import dev.code_n_roll.gatling.jdbc.action.JdbcTableCreationActionBuilder 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnDefinition 5 | import io.gatling.core.session.Expression 6 | 7 | import scala.collection.mutable.ArrayBuffer 8 | 9 | /** 10 | * Created by ronny on 10.05.17. 11 | */ 12 | case class JdbcTableCreationBuilderBase(requestName: Expression[String]) { 13 | 14 | def table(name: Expression[String]) = JdbcTableCreationColumnsStep(requestName, name) 15 | 16 | } 17 | 18 | case class JdbcTableCreationColumnsStep(requestName: Expression[String], tableName: Expression[String]) { 19 | 20 | def columns(column: ColumnDefinition, moreColumns: ColumnDefinition*) = JdbcTableCreationActionBuilder(requestName, tableName, column +: moreColumns) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/check/JdbcSimpleCheck.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.check 2 | 3 | import java.sql.ResultSet 4 | import java.util 5 | 6 | import dev.code_n_roll.gatling.jdbc.JdbcCheck 7 | import io.gatling.commons.validation.{Failure, Validation} 8 | import io.gatling.core.check.{Check, CheckResult} 9 | import io.gatling.core.session.Session 10 | import scalikejdbc.WrappedResultSet 11 | 12 | import scala.collection.mutable 13 | 14 | /** 15 | * Created by ronny on 15.05.17. 16 | */ 17 | case class JdbcSimpleCheck(func: List[Map[String, Any]] => Boolean) extends JdbcCheck[Map[String, Any]] { 18 | override def check(response: List[Map[String, Any]], session: Session, preparedCache: util.Map[Any, Any]): Validation[CheckResult] = { 19 | if (func(response)) { 20 | CheckResult.NoopCheckResultSuccess 21 | } else { 22 | Failure("JDBC check failed") 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcTableCreationActionBuilder.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnDefinition 4 | import io.gatling.core.action.Action 5 | import io.gatling.core.action.builder.ActionBuilder 6 | import io.gatling.core.session.Expression 7 | import io.gatling.core.structure.ScenarioContext 8 | 9 | import scala.collection.mutable.ArrayBuffer 10 | 11 | /** 12 | * Created by ronny on 10.05.17. 13 | */ 14 | case class JdbcTableCreationActionBuilder(requestName: Expression[String], name: Expression[String], columns: Seq[ColumnDefinition]) extends ActionBuilder { 15 | 16 | override def build(ctx: ScenarioContext, next: Action): Action = { 17 | val statsEngine = ctx.coreComponents.statsEngine 18 | val clock = ctx.coreComponents.clock 19 | JdbcCreateTableAction(requestName, name, columns, clock, statsEngine, next) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/JdbcDsl.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc 2 | 3 | import io.gatling.core.session.Expression 4 | import dev.code_n_roll.gatling.jdbc.protocol.{JdbcProtocol, JdbcProtocolBuilder, JdbcProtocolBuilderBase, JdbcProtocolBuilderConnectionPoolSettingsStep} 5 | import dev.code_n_roll.gatling.jdbc.builder.JdbcActionBuilderBase 6 | import dev.code_n_roll.gatling.jdbc.check.JdbcCheckSupport 7 | 8 | import scala.language.implicitConversions 9 | 10 | /** 11 | * Created by ronny on 10.05.17. 12 | */ 13 | trait JdbcDsl extends JdbcCheckSupport { 14 | 15 | val jdbc = JdbcProtocolBuilderBase 16 | 17 | def jdbc(requestName: Expression[String]) = JdbcActionBuilderBase(requestName) 18 | 19 | implicit def jdbcProtocolBuilder2JdbcProtocol(protocolBuilder: JdbcProtocolBuilder): JdbcProtocol = protocolBuilder.build 20 | 21 | implicit def jdbcProtocolBuilderConnectionPoolSettingsStep2JdbcProtocol(protocolBuilder: JdbcProtocolBuilderConnectionPoolSettingsStep): JdbcProtocol = protocolBuilder.build 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/builder/column/ColumnHelper.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.builder.column 2 | 3 | import io.gatling.core.session.Expression 4 | 5 | /** 6 | * Created by ronny on 11.05.17. 7 | */ 8 | object ColumnHelper { 9 | 10 | def column(name: ColumnName, dataType: ColumnDataType): ColumnDefinition = ColumnDefinition(name, dataType, None) 11 | 12 | def column(name: ColumnName, dataType: ColumnDataType, columnConstraint: ColumnConstraint): ColumnDefinition = ColumnDefinition(name, dataType, Some(columnConstraint)) 13 | 14 | def name(name: Expression[String]) = ColumnName(name) 15 | 16 | def dataType(dataType: Expression[String]) = ColumnDataType(dataType) 17 | 18 | def constraint(constraint: Expression[String]) = ColumnConstraint(constraint) 19 | 20 | } 21 | 22 | case class ColumnName(name: Expression[String]) 23 | 24 | case class ColumnDataType(dataType: Expression[String]) 25 | 26 | case class ColumnConstraint(constraint: Expression[String]) 27 | 28 | case class ColumnDefinition(name: ColumnName, dataType: ColumnDataType, columnConstraint: Option[ColumnConstraint]) 29 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcAction.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.commons.stats.{KO, OK} 4 | import io.gatling.core.action.ChainableAction 5 | import io.gatling.core.session.{Expression, Session} 6 | import io.gatling.core.stats.StatsEngine 7 | import io.gatling.core.stats.message.ResponseTimings 8 | import io.gatling.core.util.NameGen 9 | 10 | import scala.util.Try 11 | 12 | /** 13 | * Created by ronny on 12.05.17. 14 | */ 15 | trait JdbcAction extends ChainableAction with NameGen { 16 | 17 | def log(start: Long, end: Long, tried: Try[_], requestName: Expression[String], session: Session, statsEngine: StatsEngine): Session = { 18 | val (status, message) = tried match { 19 | case scala.util.Success(_) => (OK, None) 20 | case scala.util.Failure(exception) => (KO, Some(exception.getMessage)) 21 | } 22 | requestName.apply(session).foreach { resolvedRequestName => 23 | statsEngine.logResponse(session, resolvedRequestName, start, end, status, None, message) 24 | } 25 | session.logGroupRequestTimings(start, end) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/builder/JdbcInsertionBuilderBase.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.builder 2 | 3 | import dev.code_n_roll.gatling.jdbc.action.JdbcInsertionActionBuilder 4 | import io.gatling.core.session.Expression 5 | 6 | /** 7 | * Created by ronny on 11.05.17. 8 | */ 9 | case class JdbcInsertionBuilderBase(requestName: Expression[String]) { 10 | 11 | /** 12 | * The name can either be the table name (INSERT INTO table_name VALUES (...)) or the table name followed by the column names (INSERT INTO table_name (column1, ...) VALUES (...)). 13 | * For the latter one has to provide the string "table_name (column1, ...)" INCLUDING the parenthesis. 14 | */ 15 | def into(name: Expression[String]) = JdbcInsertionValuesStep(requestName, name) 16 | 17 | } 18 | 19 | case class JdbcInsertionValuesStep(requestName: Expression[String], tableName: Expression[String]) { 20 | 21 | /** 22 | * Although inserting several values is possible, they should be all in a single string. 23 | */ 24 | def values(values: Expression[String]) = JdbcInsertionActionBuilder(requestName, tableName, values) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/protocol/JdbcProtocolSpec.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.protocol 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef.jdbc 4 | import org.scalatest.{FlatSpec, Matchers} 5 | import scalikejdbc.{ConnectionPool, ConnectionPoolSettings} 6 | 7 | class JdbcProtocolSpec extends FlatSpec with Matchers { 8 | 9 | "JdbcProtocol" should "use default connection pool setting if none are provided" in { 10 | jdbc 11 | .url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE") 12 | .username("sa") 13 | .password("sa") 14 | .driver("org.h2.Driver") 15 | .build 16 | 17 | ConnectionPool.get().settings should equal(ConnectionPoolSettings()) 18 | } 19 | 20 | it should "use custom connection pool settings if they are given" in { 21 | val settings = ConnectionPoolSettings(maxSize = 20) 22 | jdbc 23 | .url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE") 24 | .username("sa") 25 | .password("sa") 26 | .driver("org.h2.Driver") 27 | .connectionPoolSettings(settings) 28 | .build 29 | 30 | ConnectionPool.get().settings should equal(settings) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/CreateTableSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef._ 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.scenario.Simulation 7 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 8 | 9 | /** 10 | * Created by ronny on 10.05.17. 11 | */ 12 | class CreateTableSimulation extends Simulation { 13 | 14 | val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") 15 | 16 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 17 | 18 | val testScenario = scenario("create table") 19 | .exec(jdbc("bar table") 20 | .create() 21 | .table("bar") 22 | .columns( 23 | column( 24 | name("abc"), 25 | dataType("INTEGER"), 26 | constraint("PRIMARY KEY") 27 | ), 28 | column( 29 | name("ac"), 30 | dataType("INTEGER") 31 | ) 32 | ) 33 | ) 34 | 35 | 36 | setUp(testScenario.inject(atOnceUsers(1))) 37 | .protocols(jdbcConfig) 38 | .assertions(global.failedRequests.count.is(0)) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/check/JdbcSingleTCheck.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.check 2 | 3 | import java.util 4 | 5 | import dev.code_n_roll.gatling.jdbc.JdbcCheck 6 | import dev.code_n_roll.gatling.jdbc.Predef.ManyAnyResult 7 | import io.gatling.commons.validation.{Validation, _} 8 | import io.gatling.core.check._ 9 | import io.gatling.core.session._ 10 | 11 | object JdbcSingleTCheck { 12 | 13 | trait JdbcSingleTCheckType 14 | 15 | def singleTPreparer[T]: Preparer[List[T], T] = something => something.head.success 16 | 17 | def singleTCheckMaterializer[T]: CheckMaterializer[JdbcSingleTCheckType, JdbcCheck[T], List[T], T] = 18 | new CheckMaterializer[JdbcSingleTCheckType, JdbcCheck[T], List[T], T](identity) { 19 | 20 | override protected def preparer: Preparer[List[T], T] = singleTPreparer[T] 21 | 22 | } 23 | 24 | def singleTExtractor[T]: Expression[Extractor[T, T]] = 25 | new Extractor[T, T] { 26 | override def name: String = "singleT" 27 | 28 | override def apply(prepared: T): Validation[Option[T]] = Some(prepared).success 29 | 30 | override def arity: String = "single" 31 | }.expressionSuccess 32 | 33 | def singleTResult[T] = new DefaultFindCheckBuilder[JdbcSingleTCheckType, T, T]( 34 | singleTExtractor[T], 35 | displayActualValue = true 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/check/JdbcManyTCheck.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.check 2 | 3 | import dev.code_n_roll.gatling.jdbc.JdbcCheck 4 | import dev.code_n_roll.gatling.jdbc.Predef.ManyAnyResult 5 | import io.gatling.commons.validation.{Validation, _} 6 | import io.gatling.core.check._ 7 | import io.gatling.core.session.{Expression, _} 8 | 9 | object JdbcManyTCheck { 10 | 11 | trait JdbcManyTCheckType 12 | 13 | def manyTPreparer[T]: Preparer[List[T], List[T]] = something => something.success 14 | 15 | def manyTCheckMaterializer[T]: CheckMaterializer[JdbcManyTCheckType, JdbcCheck[T], List[T], List[T]] = 16 | new CheckMaterializer[JdbcManyTCheckType, JdbcCheck[T], List[T], List[T]](identity) { 17 | 18 | override protected def preparer: Preparer[List[T], List[T]] = manyTPreparer 19 | 20 | } 21 | 22 | def manyTExtractor[T]: Expression[Extractor[List[T], List[T]]] = 23 | new Extractor[List[T], List[T]] { 24 | override def name: String = "manyT" 25 | 26 | override def apply(prepared: List[T]): Validation[Option[List[T]]] = Some(prepared).success 27 | 28 | override def arity: String = "findAll" 29 | }.expressionSuccess 30 | 31 | def manyTResults[T] = new DefaultFindCheckBuilder[JdbcManyTCheckType, List[T], List[T]]( 32 | manyTExtractor, 33 | displayActualValue = true 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/DropTableSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef._ 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.scenario.Simulation 7 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 8 | 9 | /** 10 | * Created by ronny on 10.05.17. 11 | */ 12 | class DropTableSimulation extends Simulation { 13 | 14 | val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") 15 | 16 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 17 | 18 | val testScenario = scenario("createTable"). 19 | exec(jdbc("bar table") 20 | .create() 21 | .table("bar") 22 | .columns( 23 | column( 24 | name("abc"), 25 | dataType("INTEGER"), 26 | constraint("PRIMARY KEY") 27 | ), 28 | column( 29 | name("ac"), 30 | dataType("INTEGER") 31 | ) 32 | ) 33 | ).exec(jdbc("drop bar table").drop().table("bar")) 34 | 35 | 36 | setUp(testScenario.inject(atOnceUsers(1))) 37 | .protocols(jdbcConfig) 38 | .assertions(global.failedRequests.count.is(0)) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcDeletionActionBuilder.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.core.action.Action 4 | import io.gatling.core.action.builder.ActionBuilder 5 | import io.gatling.core.session.Expression 6 | import io.gatling.core.structure.ScenarioContext 7 | 8 | /** 9 | * Created by ronny on 11.05.17. 10 | */ 11 | 12 | case class JdbcDeletionWithoutWhereActionBuilder(requestName: Expression[String], tableName: Expression[String]) extends ActionBuilder { 13 | 14 | def where(where: Expression[String]) = JdbcDeletionWithWhereActionBuilder(requestName, tableName, where) 15 | 16 | override def build(ctx: ScenarioContext, next: Action): Action = { 17 | val statsEngine = ctx.coreComponents.statsEngine 18 | val clock = ctx.coreComponents.clock 19 | JdbcDeletionAction(requestName, tableName, None, clock, statsEngine, next) 20 | } 21 | } 22 | 23 | case class JdbcDeletionWithWhereActionBuilder(requestName: Expression[String], tableName: Expression[String], where: Expression[String]) extends ActionBuilder { 24 | 25 | override def build(ctx: ScenarioContext, next: Action): Action = { 26 | val statsEngine = ctx.coreComponents.statsEngine 27 | val clock = ctx.coreComponents.clock 28 | JdbcDeletionAction(requestName, tableName, Some(where), clock, statsEngine, next) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/SelectSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef._ 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.scenario.Simulation 7 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 8 | 9 | /** 10 | * Created by ronny on 10.05.17. 11 | */ 12 | class SelectSimulation extends Simulation { 13 | 14 | val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") 15 | 16 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 17 | 18 | val testScenario = scenario("createTable"). 19 | exec(jdbc("bar table") 20 | .create() 21 | .table("bar") 22 | .columns( 23 | column( 24 | name("abc"), 25 | dataType("INTEGER"), 26 | constraint("PRIMARY KEY") 27 | ) 28 | ) 29 | ).repeat(10, "n") { 30 | exec(jdbc("insertion") 31 | .insert() 32 | .into("bar") 33 | .values("${n}") 34 | ) 35 | }.pause(1). 36 | exec(jdbc("selection") 37 | .select("*") 38 | .from("bar") 39 | .where("abc=4") 40 | ) 41 | 42 | 43 | setUp(testScenario.inject(atOnceUsers(1))) 44 | .protocols(jdbcConfig) 45 | .assertions(global.failedRequests.count.is(0)) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/DeleteSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef._ 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.scenario.Simulation 7 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 8 | 9 | /** 10 | * Created by ronny on 10.05.17. 11 | */ 12 | class DeleteSimulation extends Simulation { 13 | 14 | val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") 15 | 16 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 17 | 18 | val testScenario = scenario("createTable"). 19 | exec(jdbc("bar table") 20 | .create() 21 | .table("bar") 22 | .columns( 23 | column( 24 | name("abc"), 25 | dataType("INTEGER"), 26 | constraint("PRIMARY KEY") 27 | ) 28 | ) 29 | ).repeat(10, "n") { 30 | exec(jdbc("insertion") 31 | .insert() 32 | .into("bar") 33 | .values("${n}") 34 | ) 35 | }.repeat(5, "n") { 36 | exec(jdbc("deletion") 37 | .delete() 38 | .from("bar") 39 | .where("abc=${n}")) 40 | } 41 | 42 | 43 | setUp(testScenario.inject(atOnceUsers(1))) 44 | .protocols(jdbcConfig) 45 | .assertions(global.failedRequests.count.is(0)) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/check/JdbcSimpleCheckSpec.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.check 2 | 3 | import java.time.Instant 4 | import java.util 5 | 6 | import io.gatling.commons.validation.{Failure, Success} 7 | import io.gatling.core.check.CheckResult 8 | import io.gatling.core.session.Session 9 | import org.scalatest.{FlatSpec, Matchers} 10 | 11 | import scala.collection.mutable 12 | 13 | /** 14 | * Created by ronny on 15.05.17. 15 | */ 16 | class JdbcSimpleCheckSpec extends FlatSpec with Matchers { 17 | 18 | val session = Session("scenario", 0, Instant.now.getEpochSecond) 19 | 20 | implicit val cache: util.Map[Any, Any] = new util.HashMap[Any, Any]() 21 | 22 | "JdbcSimpleCheck" should "log a success if the function returns true" in { 23 | val check = JdbcSimpleCheck(_ => true) 24 | val result = check.check(List.empty, session, null) 25 | 26 | result should equal(CheckResult.NoopCheckResultSuccess) 27 | } 28 | 29 | it should "log a failure if the function returns false" in { 30 | val check = JdbcSimpleCheck(_ => false) 31 | val result = check.check(List.empty, session, null) 32 | 33 | result should equal(Failure("JDBC check failed")) 34 | } 35 | 36 | it should "provide the response list to the function" in { 37 | val list = List(Map("foo" -> "bar"), Map("bar" -> "foo")) 38 | val check = JdbcSimpleCheck(response => response eq list) 39 | val result = check.check(list, session, null) 40 | 41 | result should equal(CheckResult.NoopCheckResultSuccess) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcDropTableAction.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.commons.util.Clock 4 | import io.gatling.commons.validation.{Failure, Success} 5 | import io.gatling.core.action.Action 6 | import io.gatling.core.session.{Expression, Session} 7 | import io.gatling.core.stats.StatsEngine 8 | import scalikejdbc.{DB, SQL} 9 | 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | import scala.concurrent.Future 12 | 13 | /** 14 | * Created by ronny on 11.05.17. 15 | */ 16 | case class JdbcDropTableAction(requestName: Expression[String], 17 | tableName: Expression[String], 18 | clock: Clock, 19 | statsEngine: StatsEngine, 20 | next: Action) extends JdbcAction { 21 | 22 | override def name: String = genName("jdbcDropTable") 23 | 24 | override def execute(session: Session): Unit = { 25 | val start = clock.nowMillis 26 | val validatedTableName = tableName.apply(session) 27 | validatedTableName match { 28 | case Success(name) => 29 | val query = s"DROP TABLE $name" 30 | val future = Future { 31 | DB autoCommit { implicit session => 32 | SQL(query).map(rs => rs.toMap()).execute().apply() 33 | } 34 | } 35 | future.onComplete(result => { 36 | next ! log(start, clock.nowMillis, result, requestName, session, statsEngine) 37 | }) 38 | 39 | case Failure(error) => throw new IllegalArgumentException(error) 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/SelectCheckSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef._ 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.scenario.Simulation 7 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 8 | 9 | /** 10 | * Created by ronny on 10.05.17. 11 | */ 12 | class SelectCheckSimulation extends Simulation { 13 | 14 | val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") 15 | 16 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 17 | 18 | val testScenario = scenario("createTable"). 19 | exec(jdbc("bar table") 20 | .create() 21 | .table("bar") 22 | .columns( 23 | column( 24 | name("abc"), 25 | dataType("INTEGER"), 26 | constraint("PRIMARY KEY") 27 | ), 28 | column( 29 | name("foo"), 30 | dataType("INTEGER") 31 | ) 32 | ) 33 | ).repeat(10, "n") { 34 | exec(jdbc("insertion") 35 | .insert() 36 | .into("bar") 37 | .values("${n}, ${n}") 38 | ) 39 | }.pause(1). 40 | exec(jdbc("selection") 41 | .select("*") 42 | .from("bar") 43 | .where("abc=4") 44 | .check(simpleCheck(result => result.head("FOO") == 4)) 45 | ) 46 | 47 | 48 | setUp(testScenario.inject(atOnceUsers(1))) 49 | .protocols(jdbcConfig) 50 | .assertions(global.failedRequests.count.is(0)) 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/protocol/JdbcProtocolBuilderBase.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.protocol 2 | 3 | import scalikejdbc.ConnectionPool.CPSettings 4 | 5 | /** 6 | * Created by ronny on 10.05.17. 7 | */ 8 | case object JdbcProtocolBuilderBase { 9 | 10 | def url(url: String) = JdbcProtocolBuilderUsernameStep(url) 11 | 12 | } 13 | 14 | case class JdbcProtocolBuilderUsernameStep(url: String) { 15 | 16 | def username(name: String) = JdbcProtocolBuilderPasswordStep(url, name) 17 | 18 | } 19 | 20 | case class JdbcProtocolBuilderPasswordStep(url: String, username: String) { 21 | 22 | def password(pwd: String) = JdbcProtocolBuilderDriverStep(url, username, pwd) 23 | 24 | } 25 | 26 | case class JdbcProtocolBuilderDriverStep(url: String, username: String, password: String) { 27 | 28 | /** 29 | * The fully qualified name of the driver as usually loaded by Class.forName(...) 30 | */ 31 | def driver(driver: String) = JdbcProtocolBuilderConnectionPoolSettingsStep(url, username, password, driver) 32 | 33 | } 34 | 35 | case class JdbcProtocolBuilderConnectionPoolSettingsStep(url: String, username: String, password: String, driver: String) { 36 | 37 | def build = JdbcProtocol(url, username, password, driver, None) 38 | 39 | def connectionPoolSettings(connectionPoolSettings: CPSettings) = JdbcProtocolBuilder(url, username, password, driver, connectionPoolSettings) 40 | 41 | } 42 | 43 | case class JdbcProtocolBuilder(url: String, username: String, pwd: String, driver: String, connectionPoolSettings: CPSettings) { 44 | 45 | def build = JdbcProtocol(url, username, pwd, driver, Some(connectionPoolSettings)) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/GroupInsertSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef._ 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.scenario.Simulation 7 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 8 | 9 | import scala.concurrent.duration._ 10 | import scala.util.Random 11 | 12 | /** 13 | * Simulation to check that the reporting of times also works when using a group. 14 | * Therefore, the response time is set to be between 0 and 1000. When the group 15 | * time doesn't work the time will be just zero. 16 | */ 17 | class GroupInsertSimulation extends Simulation { 18 | 19 | val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") 20 | 21 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 22 | 23 | val scn = scenario("group create insert") 24 | .group("group") { 25 | exec(jdbc("bar table") 26 | .create() 27 | .table("bar") 28 | .columns( 29 | column( 30 | name("abc"), 31 | dataType("VARCHAR"), 32 | constraint("PRIMARY KEY") 33 | ) 34 | ) 35 | ).pause(3.seconds) 36 | .exec(jdbc("insertion") 37 | .insert() 38 | .into("bar (abc)") 39 | .values("'1'")) 40 | } 41 | 42 | setUp( 43 | scn.inject(atOnceUsers(1)), 44 | ).protocols(jdbcConfig) 45 | .assertions(details("group").responseTime.mean.between(0, 1000, inclusive = false)) 46 | } 47 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/InsertSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef._ 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.scenario.Simulation 7 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 8 | 9 | import scala.concurrent.duration._ 10 | import scala.util.Random 11 | 12 | /** 13 | * Created by ronny on 10.05.17. 14 | */ 15 | class InsertSimulation extends Simulation { 16 | 17 | val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") 18 | val feeder = Iterator.continually(Map("rand" -> (Random.alphanumeric.take(20).mkString + "@foo.com"))) 19 | 20 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 21 | 22 | val createTable = scenario("create table") 23 | .exec(jdbc("bar table") 24 | .create() 25 | .table("bar") 26 | .columns( 27 | column( 28 | name("abc"), 29 | dataType("VARCHAR"), 30 | constraint("PRIMARY KEY") 31 | ) 32 | ) 33 | ) 34 | 35 | val insertion = scenario("insertion") 36 | .pause(3.seconds) 37 | .feed(feeder) 38 | .repeat(10, "n") { 39 | exec(jdbc("insertion") 40 | .insert() 41 | .into("bar (abc)") 42 | .values("'${rand} + ${n}'") 43 | ) 44 | } 45 | 46 | setUp( 47 | createTable.inject(atOnceUsers(1)), 48 | insertion.inject(atOnceUsers(10)) 49 | ).protocols(jdbcConfig) 50 | .assertions(global.failedRequests.count.is(0)) 51 | } 52 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/protocol/JdbcProtocol.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.protocol 2 | 3 | import io.gatling.core.{CoreComponents, protocol} 4 | import io.gatling.core.config.GatlingConfiguration 5 | import io.gatling.core.protocol.{Protocol, ProtocolKey} 6 | import scalikejdbc.ConnectionPool.CPSettings 7 | import scalikejdbc._ 8 | 9 | /** 10 | * Created by ronny on 10.05.17. 11 | */ 12 | class JdbcProtocol(url: String, username: String, pwd: String, driver: String, connectionPoolSettings: Option[CPSettings]) extends Protocol { 13 | 14 | Class.forName(driver) 15 | 16 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true) 17 | 18 | if (connectionPoolSettings.isDefined) { 19 | ConnectionPool.singleton(url, username, pwd, connectionPoolSettings.get) 20 | } else { 21 | ConnectionPool.singleton(url, username, pwd) 22 | } 23 | } 24 | 25 | object JdbcProtocol { 26 | 27 | val jdbcProtocolKey: ProtocolKey[JdbcProtocol, JdbcComponents] = new ProtocolKey[JdbcProtocol, JdbcComponents] { 28 | 29 | override def protocolClass: Class[protocol.Protocol] = classOf[JdbcProtocol].asInstanceOf[Class[io.gatling.core.protocol.Protocol]] 30 | 31 | override def defaultProtocolValue(configuration: GatlingConfiguration): JdbcProtocol = 32 | throw new IllegalStateException("Can't provide a default value for JdbcProtocol") 33 | 34 | override def newComponents(coreComponents: CoreComponents): JdbcProtocol => JdbcComponents = { 35 | protocol => JdbcComponents(protocol) 36 | } 37 | 38 | } 39 | 40 | def apply(url: String, username: String, pwd: String, driver: String, connectionPoolSettings: Option[CPSettings]): JdbcProtocol = 41 | new JdbcProtocol(url, username, pwd, driver, connectionPoolSettings) 42 | } -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcInsertAction.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.commons.util.Clock 4 | import io.gatling.commons.validation.{Success, Validation} 5 | import io.gatling.core.action.Action 6 | import io.gatling.core.session.{Expression, Session} 7 | import io.gatling.core.stats.StatsEngine 8 | import scalikejdbc.{DB, SQL} 9 | 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | import scala.concurrent.Future 12 | 13 | 14 | /** 15 | * Created by ronny on 11.05.17. 16 | */ 17 | case class JdbcInsertAction(requestName: Expression[String], 18 | tableName: Expression[String], 19 | values: Expression[String], 20 | clock: Clock, 21 | statsEngine: StatsEngine, 22 | next: Action) extends JdbcAction { 23 | 24 | override def name: String = genName("jdbcInsert") 25 | 26 | override def execute(session: Session): Unit = { 27 | val start = clock.nowMillis 28 | val validatedTableName = tableName.apply(session) 29 | val validatedValues = values.apply(session) 30 | 31 | val result: Validation[Future[Unit]] = for { 32 | tableValue <- validatedTableName 33 | valuesValue <- validatedValues 34 | sql <- Success(s"INSERT INTO $tableValue VALUES ( $valuesValue )") 35 | } yield { 36 | Future { 37 | DB autoCommit { implicit session => 38 | SQL(sql).execute().apply() 39 | } 40 | } 41 | } 42 | result.foreach(_.onComplete(result => { 43 | next ! log(start, clock.nowMillis, result, requestName, session, statsEngine) 44 | })) 45 | 46 | result.onFailure(e => throw new IllegalArgumentException(e)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/action/JdbcActionSpec.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import java.time.Instant 4 | import java.util.concurrent.TimeUnit 5 | 6 | import dev.code_n_roll.gatling.jdbc.mock.MockStatsEngine 7 | import io.gatling.core.action.Action 8 | import io.gatling.core.session.Session 9 | import org.scalatest.{BeforeAndAfter, BeforeAndAfterAll, FlatSpec, Matchers} 10 | import scalikejdbc.{ConnectionPool, GlobalSettings, LoggingSQLAndTimeSettings} 11 | 12 | /** 13 | * Created by ronny on 12.05.17. 14 | */ 15 | trait JdbcActionSpec extends FlatSpec with BeforeAndAfter with BeforeAndAfterAll with Matchers { 16 | 17 | val session = Session("scenario", 0, Instant.now().getEpochSecond) 18 | val next = new Action { 19 | override def name: String = "mockAction" 20 | 21 | override def execute(session: Session): Unit = {} 22 | } 23 | val statsEngine = new MockStatsEngine 24 | 25 | override def beforeAll(): Unit = { 26 | Class.forName("org.h2.Driver") 27 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 28 | ConnectionPool.singleton("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE", "sa", "sa") 29 | } 30 | 31 | before { 32 | statsEngine.dataWriterMsg = List() 33 | } 34 | 35 | override def afterAll(): Unit = { 36 | ConnectionPool.closeAll() 37 | } 38 | 39 | 40 | def waitForLatch(latchAction: BlockingLatchAction): Unit = { 41 | latchAction.latch.await(2L, TimeUnit.SECONDS) shouldBe true 42 | } 43 | } 44 | 45 | case class NextAction(session: Session, var called: Boolean = false) extends BlockingLatchAction { 46 | override def name: String = "next Action" 47 | 48 | override def execute(s: Session): Unit = { 49 | if(s == session) called = true 50 | super.execute(s) 51 | } 52 | 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcDeletionAction.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.commons.util.Clock 4 | import io.gatling.commons.validation.{Failure, Success, Validation} 5 | import io.gatling.core.action.Action 6 | import io.gatling.core.session.{Expression, Session} 7 | import io.gatling.core.stats.StatsEngine 8 | import scalikejdbc.{DB, SQL} 9 | 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | import scala.concurrent.Future 12 | 13 | /** 14 | * Created by ronny on 11.05.17. 15 | */ 16 | case class JdbcDeletionAction(requestName: Expression[String], 17 | tableName: Expression[String], 18 | where: Option[Expression[String]], 19 | clock: Clock, 20 | statsEngine: StatsEngine, 21 | next: Action) extends JdbcAction { 22 | 23 | override def name: String = genName("jdbcDelete") 24 | 25 | override def execute(session: Session): Unit = { 26 | val start = clock.nowMillis 27 | val validatedTableName = tableName.apply(session) 28 | 29 | val wherePart = where.fold("")(_.apply(session) match { 30 | case Success(whereString) => s"WHERE $whereString" 31 | case Failure(e) => throw new IllegalArgumentException(e) 32 | }) 33 | 34 | val result: Validation[Future[Unit]] = for { 35 | tableString <- validatedTableName 36 | sqlString <- Success(s"DELETE FROM $tableString $wherePart") 37 | } yield Future { 38 | DB autoCommit { implicit session => 39 | SQL(sqlString).map(rs => rs.toMap()).execute().apply() 40 | } 41 | } 42 | 43 | result.foreach(_.onComplete(result => { 44 | next ! log(start, clock.nowMillis, result, requestName, session, statsEngine) 45 | })) 46 | 47 | result.onFailure(e => throw new IllegalArgumentException(e)) 48 | } 49 | } -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/InsertMySqlSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import java.util.UUID 4 | 5 | import dev.code_n_roll.gatling.jdbc.Predef._ 6 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 7 | import io.gatling.core.Predef._ 8 | import io.gatling.core.scenario.Simulation 9 | import org.testcontainers.containers.MySQLContainer 10 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 11 | 12 | /** 13 | * Created by ronny on 10.05.17. 14 | */ 15 | class InsertMySqlSimulation extends Simulation { 16 | 17 | val mySql = new MySQLContainer() 18 | mySql.start() 19 | 20 | val jdbcConfig = jdbc.url(mySql.getJdbcUrl).username(mySql.getUsername).password(mySql.getPassword).driver(mySql.getDriverClassName) 21 | 22 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 23 | 24 | val tableIdentFeeder = for (x <- 0 until 10) yield Map("tableId" -> x) 25 | 26 | val uniqueValuesFeeder = Iterator.continually(Map("unique" -> UUID.randomUUID())) 27 | 28 | after { 29 | mySql.stop() 30 | } 31 | 32 | val createTables = scenario("createTable").feed(tableIdentFeeder.iterator). 33 | exec(jdbc("create") 34 | .create() 35 | .table("bar${tableId}") 36 | .columns( 37 | column( 38 | name("abc"), 39 | dataType("VARCHAR(39)"), 40 | constraint("PRIMARY KEY") 41 | ) 42 | ) 43 | ) 44 | 45 | val fillTables = feed(uniqueValuesFeeder).repeat(100, "n") { 46 | feed(tableIdentFeeder.iterator.toArray.random). 47 | exec(jdbc("insertion") 48 | .insert() 49 | .into("bar${tableId}") 50 | .values("'${unique}${n}'") 51 | ) 52 | } 53 | 54 | 55 | setUp( 56 | createTables.inject(atOnceUsers(10)), 57 | scenario("fillTables").pause(5).exec(fillTables).inject(atOnceUsers(100)) 58 | ).protocols(jdbcConfig) 59 | .assertions(global.successfulRequests.percent.gte(99)) 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/InsertPostgresSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import java.util.UUID 4 | 5 | import dev.code_n_roll.gatling.jdbc.Predef._ 6 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 7 | import io.gatling.core.Predef._ 8 | import io.gatling.core.scenario.Simulation 9 | import org.testcontainers.containers.PostgreSQLContainer 10 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 11 | 12 | /** 13 | * Created by ronny on 10.05.17. 14 | */ 15 | class InsertPostgresSimulation extends Simulation { 16 | 17 | val postgres = new PostgreSQLContainer() 18 | postgres.start() 19 | 20 | val jdbcConfig = jdbc.url(postgres.getJdbcUrl).username(postgres.getUsername).password(postgres.getPassword).driver(postgres.getDriverClassName) 21 | 22 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 23 | 24 | val tableIdentFeeder = for (x <- 0 until 10) yield Map("tableId" -> x) 25 | 26 | val uniqueValuesFeeder = Iterator.continually(Map("unique" -> UUID.randomUUID())) 27 | 28 | after { 29 | postgres.stop() 30 | } 31 | 32 | val createTables = scenario("createTable").feed(tableIdentFeeder.iterator). 33 | exec(jdbc("create") 34 | .create() 35 | .table("bar${tableId}") 36 | .columns( 37 | column( 38 | name("abc"), 39 | dataType("VARCHAR(39)"), 40 | constraint("PRIMARY KEY") 41 | ) 42 | ) 43 | ) 44 | 45 | val fillTables = feed(uniqueValuesFeeder).repeat(100, "n") { 46 | feed(tableIdentFeeder.iterator.toArray.random). 47 | exec(jdbc("insertion") 48 | .insert() 49 | .into("bar${tableId}") 50 | .values("'${unique}${n}'") 51 | ) 52 | } 53 | 54 | 55 | setUp( 56 | createTables.inject(atOnceUsers(10)), 57 | scenario("fillTables").pause(5).exec(fillTables).inject(atOnceUsers(100)) 58 | ).protocols(jdbcConfig) 59 | .assertions(global.successfulRequests.percent.gte(99)) 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/simulation/SelectTypedCheckSimulation.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.simulation 2 | 3 | import dev.code_n_roll.gatling.jdbc.Predef._ 4 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.scenario.Simulation 7 | import scalikejdbc.{GlobalSettings, LoggingSQLAndTimeSettings} 8 | 9 | /** 10 | * Created by ronny on 10.05.17. 11 | */ 12 | class SelectTypedCheckSimulation extends Simulation { 13 | 14 | val jdbcConfig = jdbc.url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE").username("sa").password("sa").driver("org.h2.Driver") 15 | 16 | GlobalSettings.loggingSQLAndTime = LoggingSQLAndTimeSettings(singleLineMode = true, logLevel = 'warn) 17 | 18 | val testScenario = scenario("createTable"). 19 | exec(jdbc("bar table") 20 | .create() 21 | .table("bar") 22 | .columns( 23 | column( 24 | name("abc"), 25 | dataType("INTEGER"), 26 | constraint("PRIMARY KEY") 27 | ), 28 | column( 29 | name("foo"), 30 | dataType("INTEGER") 31 | ) 32 | ) 33 | ).repeat(10, "n") { 34 | exec(jdbc("insertion") 35 | .insert() 36 | .into("bar") 37 | .values("${n}, ${n}") 38 | ) 39 | }.pause(1). 40 | exec(jdbc("selectionSingleCheck") 41 | .select("*") 42 | .from("bar") 43 | .where("abc=4") 44 | .mapResult(rs => Stored(rs.int("abc"), rs.int("foo"))) 45 | .check(singleResponse[Stored].is(Stored(4, 4)) 46 | .saveAs("myResult")) 47 | ).pause(1). 48 | exec(jdbc("selectionManyCheck") 49 | .select("*") 50 | .from("bar") 51 | .where("abc=4 OR abc=5") 52 | .mapResult(rs => Stored(rs.int("abc"), rs.int("foo"))) 53 | .check(manyResponse[Stored].is(List( 54 | Stored(4, 4), 55 | Stored(5, 5))) 56 | ) 57 | ) 58 | //.exec(session => session("something").as[List[Map[String, Any]]]) 59 | 60 | 61 | setUp(testScenario.inject(atOnceUsers(1))) 62 | .protocols(jdbcConfig) 63 | .assertions(global.failedRequests.count.is(0)) 64 | 65 | } 66 | 67 | case class Stored(abc: Int, foo: Int) -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcCreateTableAction.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnDefinition 4 | import io.gatling.commons.util.Clock 5 | import io.gatling.commons.validation.{Failure, Success} 6 | import io.gatling.core.action.Action 7 | import io.gatling.core.session.{Expression, Session} 8 | import io.gatling.core.stats.StatsEngine 9 | import scalikejdbc._ 10 | 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | import scala.concurrent.Future 13 | 14 | /** 15 | * Created by ronny on 10.05.17. 16 | */ 17 | case class JdbcCreateTableAction(requestName: Expression[String], 18 | tableName: Expression[String], 19 | columns: Seq[ColumnDefinition], 20 | clock: Clock, 21 | statsEngine: StatsEngine, 22 | next: Action) extends JdbcAction { 23 | 24 | override def name: String = genName("jdbcCreateTable") 25 | 26 | override def execute(session: Session): Unit = { 27 | val start = clock.nowMillis 28 | val columnStrings = columns.map( 29 | t => ( 30 | t.name.name.apply(session), 31 | t.dataType.dataType.apply(session), 32 | t.columnConstraint.map(_.constraint(session)).getOrElse(Success("")))) 33 | .map { 34 | case (Success(columnName), Success(dataType), Success(constraint)) => s"$columnName $dataType $constraint" 35 | case _ => throw new IllegalArgumentException 36 | }.mkString(",") 37 | 38 | val validatedTableName = tableName.apply(session) 39 | validatedTableName match { 40 | case Success(name) => 41 | val query = s"CREATE TABLE $name($columnStrings)" 42 | val future = Future { 43 | DB autoCommit { implicit session => 44 | SQL(query).execute().apply() 45 | } 46 | } 47 | future.onComplete(result => { 48 | next ! log(start, clock.nowMillis, result, requestName, session, statsEngine) 49 | }) 50 | 51 | case Failure(error) => throw new IllegalArgumentException(error) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/check/JdbcCheckSupport.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.check 2 | 3 | import dev.code_n_roll.gatling.jdbc.JdbcCheck 4 | import dev.code_n_roll.gatling.jdbc.Predef.ManyAnyResult 5 | import io.gatling.core.check._ 6 | 7 | import scala.annotation.implicitNotFound 8 | 9 | /** 10 | * Created by ronny on 15.05.17. 11 | */ 12 | trait JdbcCheckSupport { 13 | 14 | def simpleCheck = JdbcSimpleCheck 15 | 16 | @Deprecated 17 | val jdbcSingleResponse = singleResponse[Map[String, Any]] 18 | 19 | def singleResponse[T]: DefaultFindCheckBuilder[JdbcSingleTCheck.JdbcSingleTCheckType, T, T] = JdbcSingleTCheck.singleTResult[T] 20 | 21 | implicit def jdbcSingleTCheckMaterializer[T]: CheckMaterializer[JdbcSingleTCheck.JdbcSingleTCheckType, JdbcCheck[T], List[T], T] = JdbcSingleTCheck.singleTCheckMaterializer[T] 22 | 23 | @Deprecated 24 | val jdbcManyResponse = manyResponse[Map[String, Any]] 25 | 26 | def manyResponse[T]: DefaultFindCheckBuilder[JdbcManyTCheck.JdbcManyTCheckType, List[T], List[T]] = JdbcManyTCheck.manyTResults[T] 27 | 28 | implicit def jdbcTCheckMaterializer[T]: CheckMaterializer[JdbcManyTCheck.JdbcManyTCheckType, JdbcCheck[T], List[T], List[T]] = JdbcManyTCheck.manyTCheckMaterializer[T] 29 | 30 | @implicitNotFound("Could not find a CheckMaterializer. This check might not be valid for JDBC.") 31 | implicit def findCheckBuilder2JdbcCheck[A, P, X](findCheckBuilder: FindCheckBuilder[A, P, X])(implicit CheckMaterializer: CheckMaterializer[A, JdbcCheck[P], List[P], P]): JdbcCheck[P] = 32 | findCheckBuilder.find.exists 33 | 34 | @implicitNotFound("Could not find a CheckMaterializer. This check might not be valid for JDBC.") 35 | implicit def checkBuilder2JdbcCheck[A, P, X](checkBuilder: CheckBuilder[A, P, X])(implicit materializer: CheckMaterializer[A, JdbcCheck[P], List[P], P]): JdbcCheck[P] = 36 | checkBuilder.build(materializer) 37 | 38 | @implicitNotFound("Could not find a CheckMaterializer. This check might not be valid for JDBC.") 39 | implicit def findManyCheckBuilder2JdbcCheck[A, P, X](findCheckBuilder: FindCheckBuilder[A, List[P], X])(implicit CheckMaterializer: CheckMaterializer[A, JdbcCheck[P], List[P], List[P]]): JdbcCheck[P] = 40 | findCheckBuilder.find.exists 41 | 42 | @implicitNotFound("Could not find a CheckMaterializer. This check might not be valid for JDBC.") 43 | implicit def checkManyBuilder2JdbcCheck[A, P, X](checkBuilder: CheckBuilder[A, List[P], X])(implicit materializer: CheckMaterializer[A, JdbcCheck[P], List[P], List[P]]): JdbcCheck[P] = 44 | checkBuilder.build(materializer) 45 | 46 | } -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcSelectionActionBuilder.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import dev.code_n_roll.gatling.jdbc.check.JdbcCheckActionBuilder 4 | import io.gatling.core.action.Action 5 | import io.gatling.core.action.builder.ActionBuilder 6 | import io.gatling.core.session.Expression 7 | import io.gatling.core.structure.ScenarioContext 8 | import scalikejdbc.WrappedResultSet 9 | 10 | /** 11 | * Created by ronny on 11.05.17. 12 | */ 13 | case class JdbcSelectionWithoutWhereActionBuilder(requestName: Expression[String], what: Expression[String], from: Expression[String]) extends JdbcCheckActionBuilder[Map[String, Any]] { 14 | 15 | def where(where: Expression[String]) = JdbcSelectionWithWhereActionBuilder(requestName, what, from, where) 16 | 17 | def mapResult[T](mapFunction: WrappedResultSet => T) = JdbcSelectionWithMappingActionBuilder(requestName, what, from, None, mapFunction) 18 | 19 | override def build(ctx: ScenarioContext, next: Action): Action = { 20 | val statsEngine = ctx.coreComponents.statsEngine 21 | val clock = ctx.coreComponents.clock 22 | JdbcSelectAction(requestName, what, from, None, checks.toList, _.toMap(), clock, statsEngine, next) 23 | } 24 | 25 | } 26 | 27 | case class JdbcSelectionWithWhereActionBuilder(requestName: Expression[String], what: Expression[String], from: Expression[String], where: Expression[String]) extends JdbcCheckActionBuilder[Map[String, Any]] { 28 | 29 | def mapResult[T](mapFunction: WrappedResultSet => T) = JdbcSelectionWithMappingActionBuilder(requestName, what, from, Some(where), mapFunction) 30 | 31 | override def build(ctx: ScenarioContext, next: Action): Action = { 32 | val statsEngine = ctx.coreComponents.statsEngine 33 | val clock = ctx.coreComponents.clock 34 | JdbcSelectAction(requestName, what, from, Some(where), checks.toList, _.toMap(), clock, statsEngine, next) 35 | } 36 | 37 | } 38 | 39 | case class JdbcSelectionWithMappingActionBuilder[T](requestName: Expression[String], 40 | what: Expression[String], 41 | from: Expression[String], 42 | where: Option[Expression[String]], 43 | mapFunction: WrappedResultSet => T) extends JdbcCheckActionBuilder[T] { 44 | 45 | override def build(ctx: ScenarioContext, next: Action): Action = { 46 | val statsEngine = ctx.coreComponents.statsEngine 47 | val clock = ctx.coreComponents.clock 48 | JdbcSelectAction(requestName, what, from, where, checks.toList, mapFunction, clock, statsEngine, next) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/mock/MockStatsEngine.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2011-2017 GatlingCorp (http://gatling.io) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package dev.code_n_roll.gatling.jdbc.mock 17 | 18 | import java.util.Date 19 | 20 | import io.gatling.commons.stats.Status 21 | import io.gatling.core.session.{GroupBlock, Session} 22 | import io.gatling.core.stats.StatsEngine 23 | import io.gatling.core.stats.message.ResponseTimings 24 | import io.gatling.core.stats.writer._ 25 | import akka.actor.ActorRef 26 | import com.typesafe.scalalogging.StrictLogging 27 | 28 | class MockStatsEngine extends StatsEngine with StrictLogging { 29 | 30 | var dataWriterMsg: List[DataWriterMessage] = List() 31 | 32 | override def start(): Unit = {} 33 | 34 | override def stop(replyTo: ActorRef, exception: Option[Exception]): Unit = {} 35 | 36 | override def logUser(userMessage: UserMessage): Unit = {} 37 | 38 | override def logResponse( 39 | session: Session, 40 | requestName: String, 41 | startTimestamp: Long, 42 | endTimestamp: Long, 43 | status: Status, 44 | responseCode: Option[String], 45 | message: Option[String] 46 | ): Unit = 47 | handle(ResponseMessage( 48 | session.scenario, 49 | session.userId, 50 | session.groupHierarchy, 51 | requestName, 52 | startTimestamp, 53 | endTimestamp, 54 | status, 55 | None, 56 | message 57 | )) 58 | 59 | override def logGroupEnd(session: Session, group: GroupBlock, exitTimestamp: Long): Unit = 60 | handle(GroupMessage(session.scenario, session.userId, group.hierarchy, group.startTimestamp, exitTimestamp, group.cumulatedResponseTime, group.status)) 61 | 62 | override def logCrash(session: Session, requestName: String, error: String): Unit = 63 | handle(ErrorMessage(error, new Date().getTime)) 64 | 65 | override def reportUnbuildableRequest(session: Session, requestName: String, errorMessage: String): Unit = {} 66 | 67 | private def handle(msg: DataWriterMessage) = { 68 | dataWriterMsg = msg :: dataWriterMsg 69 | logger.info(msg.toString) 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/scala/dev/code_n_roll/gatling/jdbc/action/JdbcSelectAction.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import dev.code_n_roll.gatling.jdbc.JdbcCheck 4 | import io.gatling.commons.stats.KO 5 | import io.gatling.commons.util.Clock 6 | import io.gatling.commons.validation.Success 7 | import io.gatling.core.action.Action 8 | import io.gatling.core.check.Check 9 | import io.gatling.core.session.{Expression, Session} 10 | import io.gatling.core.stats.StatsEngine 11 | import scalikejdbc.{DB, SQL, WrappedResultSet} 12 | 13 | import scala.concurrent.ExecutionContext.Implicits.global 14 | import scala.concurrent.Future 15 | import scala.util.{Failure, Try} 16 | 17 | /** 18 | * Created by ronny on 11.05.17. 19 | */ 20 | case class JdbcSelectAction[T](requestName: Expression[String], 21 | what: Expression[String], 22 | from: Expression[String], 23 | where: Option[Expression[String]], 24 | checks: List[JdbcCheck[T]], 25 | mapFunction: WrappedResultSet => T, 26 | clock: Clock, 27 | statsEngine: StatsEngine, 28 | next: Action) extends JdbcAction { 29 | 30 | override def name: String = genName("jdbcSelect") 31 | 32 | override def execute(session: Session): Unit = { 33 | val start = clock.nowMillis 34 | val validatedWhat = what.apply(session) 35 | val validatedFrom = from.apply(session) 36 | val validatedWhere = where.map(w => w.apply(session)) 37 | 38 | val sqlString = (validatedWhat, validatedFrom, validatedWhere) match { 39 | case (Success(whatString), Success(fromString), Some(Success(whereString))) => s"SELECT $whatString FROM $fromString WHERE $whereString" 40 | case (Success(whatString), Success(fromString), None) => s"SELECT $whatString FROM $fromString" 41 | case _ => throw new IllegalArgumentException 42 | } 43 | 44 | val future: Future[List[T]] = Future { 45 | DB autoCommit { implicit session => 46 | SQL(sqlString).map(mapFunction).toList().apply() 47 | } 48 | } 49 | future.onComplete { 50 | case scala.util.Success(value) => 51 | next ! Try(performChecks(session, start, value)).recover { 52 | case err => 53 | val logRequestName = requestName(session).toOption.getOrElse("JdbcSelectAction") 54 | statsEngine.logCrash(session, logRequestName, err.getMessage) 55 | session.markAsFailed 56 | }.get 57 | case fail: Failure[_] => 58 | next ! log(start, clock.nowMillis, fail, requestName, session, statsEngine) 59 | } 60 | } 61 | 62 | private def performChecks(session: Session, start: Long, tried: List[T]): Session = { 63 | val (modifiedSession, error) = Check.check[List[T]](tried, session, checks, null) 64 | error match { 65 | case Some(failure) => 66 | requestName.apply(session).map { resolvedRequestName => 67 | statsEngine.logResponse(session, resolvedRequestName, start, clock.nowMillis, KO, None, None) 68 | } 69 | modifiedSession.markAsFailed 70 | case _ => 71 | log(start, clock.nowMillis, scala.util.Success(""), requestName, modifiedSession, statsEngine) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/action/JdbcDropTableActionSpec.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import io.gatling.commons.stats.{KO, OK} 6 | import io.gatling.commons.util.DefaultClock 7 | import io.gatling.core.Predef._ 8 | import io.gatling.core.stats.writer.ResponseMessage 9 | import scalikejdbc._ 10 | /** 11 | * Created by ronny on 15.05.17. 12 | */ 13 | class JdbcDropTableActionSpec extends JdbcActionSpec { 14 | 15 | private val clock = new DefaultClock 16 | 17 | "JdbcDropTableAction" should "use the request name in the log message" in { 18 | val requestName = "request" 19 | val latchAction = BlockingLatchAction() 20 | val action = JdbcDropTableAction(requestName, "table", clock, statsEngine, latchAction) 21 | 22 | action.execute(session) 23 | 24 | waitForLatch(latchAction) 25 | statsEngine.dataWriterMsg should have length 1 26 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].name should equal(requestName) 27 | } 28 | 29 | it should "drop the table with the specified name" in { 30 | DB autoCommit{ implicit session => 31 | sql"""CREATE TABLE delete_me(id INTEGER PRIMARY KEY )""".execute().apply() 32 | } 33 | val latchAction = BlockingLatchAction() 34 | val action = JdbcDropTableAction("deleteRequest", "DELETE_ME", clock, statsEngine, latchAction) 35 | 36 | action.execute(session) 37 | 38 | waitForLatch(latchAction) 39 | val result = DB readOnly { implicit session => 40 | sql"""SELECT * FROM information_schema.tables WHERE TABLE_NAME = 'DELETE_ME' """.map(rs => rs.toMap()).single().apply() 41 | } 42 | result should be(empty) 43 | } 44 | 45 | it should "throw an IAE if the expression cannot be resolved" in { 46 | val action = JdbcDropTableAction("deleteRequest", "${table}", clock, statsEngine, next) 47 | 48 | an[IllegalArgumentException] shouldBe thrownBy(action.execute(session)) 49 | } 50 | 51 | it should "log an OK value when being successful" in { 52 | DB autoCommit{ implicit session => 53 | sql"""CREATE TABLE delete_other(id INTEGER PRIMARY KEY )""".execute().apply() 54 | } 55 | val latchAction = BlockingLatchAction() 56 | val action = JdbcDropTableAction("deleteRequest", "DELETE_OTHER", clock, statsEngine, latchAction) 57 | 58 | action.execute(session) 59 | 60 | waitForLatch(latchAction) 61 | statsEngine.dataWriterMsg should have length 1 62 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(OK) 63 | } 64 | 65 | it should "log a KO value when being unsuccessful" in { 66 | val latchAction = BlockingLatchAction() 67 | val action = JdbcDropTableAction("deleteRequest", "DELETE_YOU", clock, statsEngine, latchAction) 68 | 69 | action.execute(session) 70 | 71 | waitForLatch(latchAction) 72 | statsEngine.dataWriterMsg should have length 1 73 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(KO) 74 | } 75 | 76 | it should "pass the session to the next action" in { 77 | val nextAction = NextAction(session) 78 | val action = JdbcDropTableAction("deleteRequest", "DELETE_SOMETHING", clock, statsEngine, nextAction) 79 | 80 | action.execute(session) 81 | 82 | waitForLatch(nextAction) 83 | nextAction.called should be(true) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/action/JdbcInsertActionSpec.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.commons.stats.{KO, OK} 4 | import io.gatling.commons.util.DefaultClock 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.stats.writer.ResponseMessage 7 | import scalikejdbc._ 8 | 9 | /** 10 | * Created by ronny on 15.05.17. 11 | */ 12 | class JdbcInsertActionSpec extends JdbcActionSpec { 13 | 14 | private val clock = new DefaultClock 15 | 16 | "JdbcInsertAction" should "use the request name in the log message" in { 17 | val requestName = "request" 18 | val latchAction = BlockingLatchAction() 19 | val action = JdbcInsertAction(requestName, "table", "", clock, statsEngine, latchAction) 20 | 21 | action.execute(session) 22 | 23 | waitForLatch(latchAction) 24 | statsEngine.dataWriterMsg should have length 1 25 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].name should equal(requestName) 26 | } 27 | 28 | it should "insert the specified values" in { 29 | DB autoCommit { implicit session => 30 | sql"""CREATE TABLE insert_me(id INTEGER PRIMARY KEY )""".execute().apply() 31 | } 32 | val latchAction = BlockingLatchAction() 33 | val action = JdbcInsertAction("insert", "INSERT_ME", "42", clock, statsEngine, latchAction) 34 | 35 | action.execute(session) 36 | 37 | waitForLatch(latchAction) 38 | val result = DB readOnly { implicit session => 39 | sql"""SELECT * FROM INSERT_ME WHERE id = 42 """.map(rs => rs.toMap()).single().apply() 40 | } 41 | result should not be empty 42 | } 43 | 44 | it should "log an OK value when being successful" in { 45 | DB autoCommit { implicit session => 46 | sql"""CREATE TABLE insert_again(id INTEGER PRIMARY KEY )""".execute().apply() 47 | } 48 | val latchAction = BlockingLatchAction() 49 | val action = JdbcInsertAction("insert", "INSERT_AGAIN", "42", clock, statsEngine, latchAction) 50 | 51 | action.execute(session) 52 | 53 | waitForLatch(latchAction) 54 | statsEngine.dataWriterMsg should have length 1 55 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(OK) 56 | } 57 | 58 | it should "log a KO value when being unsuccessful" in { 59 | val latchAction = BlockingLatchAction() 60 | val action = JdbcInsertAction("insert", "INSERT_NOBODY", "42", clock, statsEngine, latchAction) 61 | 62 | action.execute(session) 63 | 64 | waitForLatch(latchAction) 65 | statsEngine.dataWriterMsg should have length 1 66 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(KO) 67 | } 68 | 69 | it should "throw an IAE when it cannot evaluate the table expression" in { 70 | val action = JdbcInsertAction("insert", "${table}", "42", clock, statsEngine, next) 71 | 72 | an[IllegalArgumentException] should be thrownBy action.execute(session) 73 | } 74 | 75 | it should "throw an IAE when it cannot evaluate the value expression" in { 76 | val action = JdbcInsertAction("insert", "table", "${value}", clock, statsEngine, next) 77 | 78 | an[IllegalArgumentException] should be thrownBy action.execute(session) 79 | } 80 | 81 | it should "pass the session to the next action" in { 82 | DB autoCommit { implicit session => 83 | sql"""CREATE TABLE insert_next(id INTEGER PRIMARY KEY )""".execute().apply() 84 | } 85 | val nextAction = NextAction(session) 86 | val action = JdbcInsertAction("insert", "INSERT_NEXT", "42", clock, statsEngine, nextAction) 87 | 88 | action.execute(session) 89 | 90 | waitForLatch(nextAction) 91 | nextAction.called should be(true) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/action/JdbcDeletionActionSpec.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import io.gatling.commons.stats.{KO, OK} 4 | import io.gatling.commons.util.DefaultClock 5 | import io.gatling.core.stats.writer.ResponseMessage 6 | import org.scalatest.Matchers.equal 7 | import org.scalatest.Matchers._ 8 | import io.gatling.core.Predef._ 9 | import io.gatling.core.action.Action 10 | import io.gatling.core.session.Session 11 | import scalikejdbc.DB 12 | import scalikejdbc._ 13 | 14 | /** 15 | * Created by ronny on 12.05.17. 16 | */ 17 | class JdbcDeletionActionSpec extends JdbcActionSpec { 18 | 19 | private val clock = new DefaultClock 20 | 21 | "JdbcDeletionAction" should "use the request name in the log message" in { 22 | val requestName = "name" 23 | val latchAction = BlockingLatchAction() 24 | val action = JdbcDeletionAction(requestName, "table", None, clock, statsEngine, latchAction) 25 | 26 | action.execute(session) 27 | 28 | waitForLatch(latchAction) 29 | statsEngine.dataWriterMsg should have length 1 30 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].name should equal(requestName) 31 | } 32 | 33 | it should "delete the data specied by the where clause" in { 34 | DB autoCommit { implicit session => 35 | sql"""CREATE TABLE foo(bar INTEGER ); INSERT INTO foo VALUES (1);INSERT INTO foo VALUES (2)""".execute().apply() 36 | } 37 | val latchAction = BlockingLatchAction() 38 | val action = JdbcDeletionAction("request", "foo", Some("bar = 2"), clock, statsEngine, latchAction) 39 | 40 | action.execute(session) 41 | 42 | waitForLatch(latchAction) 43 | val result = DB readOnly { implicit session => 44 | sql"""SELECT COUNT(*) FROM foo""".map(rs => rs.int(1)).single().apply() 45 | } 46 | result should contain(1) 47 | } 48 | 49 | it should "delete all data when no where specified" in { 50 | DB autoCommit { implicit session => 51 | sql"""CREATE TABLE bar(foo INTEGER ); INSERT INTO bar VALUES (1);INSERT INTO bar VALUES (2)""".execute().apply() 52 | } 53 | val latchAction = BlockingLatchAction() 54 | val action = JdbcDeletionAction("request", "bar", None, clock, statsEngine, latchAction) 55 | 56 | action.execute(session) 57 | 58 | waitForLatch(latchAction) 59 | val result = DB readOnly { implicit session => 60 | sql"""SELECT COUNT(*) FROM bar""".map(rs => rs.int(1)).single().apply() 61 | } 62 | result should contain(0) 63 | } 64 | 65 | it should "log an OK value when being successful" in { 66 | DB autoCommit { implicit session => 67 | sql"""CREATE TABLE table_1(bar INTEGER ); INSERT INTO table_1 VALUES (1);""".execute().apply() 68 | } 69 | val latchAction = BlockingLatchAction() 70 | val action = JdbcDeletionAction("request", "table_1", Some("bar = 1"), clock, statsEngine, latchAction) 71 | 72 | action.execute(session) 73 | 74 | waitForLatch(latchAction) 75 | statsEngine.dataWriterMsg should have length 1 76 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(OK) 77 | } 78 | 79 | it should "log an KO value when being unsuccessful" in { 80 | val latchAction = BlockingLatchAction() 81 | val action = JdbcDeletionAction("request", "non_existing", Some("bar = 1"), clock, statsEngine, latchAction) 82 | 83 | action.execute(session) 84 | 85 | waitForLatch(latchAction) 86 | statsEngine.dataWriterMsg should have length 1 87 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(KO) 88 | } 89 | 90 | it should "throw an IAE when the table name cannot be resolved" in { 91 | val action = JdbcDeletionAction("request", "${what}", Some("bar = 1"), clock, statsEngine, next) 92 | 93 | an[IllegalArgumentException] should be thrownBy action.execute(session) 94 | } 95 | 96 | it should "throw an IAE when the where clause cannot be resolved" in { 97 | val action = JdbcDeletionAction("request", "what", Some("${bar} = 1"), clock, statsEngine, next) 98 | 99 | an[IllegalArgumentException] should be thrownBy action.execute(session) 100 | } 101 | 102 | it should "pass the session to the next action" in { 103 | val nextAction = NextAction(session) 104 | DB autoCommit { implicit session => 105 | sql"""CREATE TABLE what(nothing INTEGER )""".execute().apply() 106 | } 107 | val action = JdbcDeletionAction("request", "what", None, clock, statsEngine, nextAction) 108 | 109 | action.execute(session) 110 | 111 | waitForLatch(nextAction) 112 | nextAction.called should be(true) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/action/JdbcCreateTableActionSpec.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._ 4 | import io.gatling.commons.stats.{KO, OK} 5 | import io.gatling.commons.util.DefaultClock 6 | import io.gatling.core.Predef._ 7 | import io.gatling.core.action.Action 8 | import io.gatling.core.session.Session 9 | import io.gatling.core.stats.writer.ResponseMessage 10 | import org.scalatest.Matchers._ 11 | import org.scalatest._ 12 | import scalikejdbc._ 13 | 14 | /** 15 | * Created by ronny on 12.05.17. 16 | */ 17 | class JdbcCreateTableActionSpec extends JdbcActionSpec { 18 | 19 | private val clock = new DefaultClock 20 | 21 | "JdbcCreateTableAction" should "use the request name in the log message" in { 22 | val requestName = "name" 23 | val latchAction = BlockingLatchAction() 24 | val action = JdbcCreateTableAction(requestName, "table", Seq(column(name("foo"), dataType("INTEGER"))), clock, statsEngine, latchAction) 25 | 26 | action.execute(session) 27 | 28 | waitForLatch(latchAction) 29 | statsEngine.dataWriterMsg should have length 1 30 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].name should equal(requestName) 31 | } 32 | 33 | it should "create the table with given name and columns" in { 34 | val action = JdbcCreateTableAction("request", "new_table", Seq(column(name("foo"), dataType("INTEGER"), constraint("PRIMARY KEY"))), clock, statsEngine, next) 35 | 36 | action.execute(session) 37 | 38 | val result = DB readOnly { implicit session => 39 | sql"""SELECT * FROM information_schema.tables WHERE TABLE_NAME = 'NEW_TABLE' """.map(rs => rs.toMap()).single().apply() 40 | } 41 | result should not be empty 42 | } 43 | 44 | it should "log an OK message when successfully creating the table" in { 45 | val latchAction = BlockingLatchAction() 46 | val action = JdbcCreateTableAction("request", "ok_table", Seq(column(name("foo"), dataType("INTEGER"), constraint("PRIMARY KEY"))), clock, statsEngine, latchAction) 47 | 48 | action.execute(session) 49 | 50 | waitForLatch(latchAction) 51 | statsEngine.dataWriterMsg should have length 1 52 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(OK) 53 | } 54 | 55 | it should "log a KO message if an error occurs" in { 56 | val latchAction = BlockingLatchAction() 57 | val latchAction2 = BlockingLatchAction() 58 | val action = JdbcCreateTableAction("request", "ko_table", Seq(column(name("foo"), dataType("INTEGER"), constraint("PRIMARY KEY"))), clock, statsEngine, latchAction) 59 | val action2 = JdbcCreateTableAction("request", "ko_table", Seq(column(name("foo"), dataType("INTEGER"), constraint("PRIMARY KEY"))), clock, statsEngine, latchAction2) 60 | 61 | action.execute(session) 62 | waitForLatch(latchAction) 63 | action2.execute(session) 64 | 65 | waitForLatch(latchAction2) 66 | statsEngine.dataWriterMsg should have length 2 67 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(KO) 68 | } 69 | 70 | it should "throw an IAE when the column name cannot be validated" in { 71 | val action = JdbcCreateTableAction("request", "exc_table", Seq(column(name("${foo}"), dataType("INTEGER"), constraint("PRIMARY KEY"))), clock, statsEngine, next) 72 | 73 | an[IllegalArgumentException] should be thrownBy action.execute(session) 74 | } 75 | 76 | it should "throw an IAE when the column data type cannot be validated" in { 77 | val action = JdbcCreateTableAction("request", "exc_table", Seq(column(name("foo"), dataType("${INTEGER}"), constraint("PRIMARY KEY"))), clock, statsEngine, next) 78 | 79 | an[IllegalArgumentException] should be thrownBy action.execute(session) 80 | } 81 | 82 | it should "throw an IAE when the column constraint cannot be validated" in { 83 | val action = JdbcCreateTableAction("request", "exc_table", Seq(column(name("foo"), dataType("INTEGER"), constraint("${constraint}"))), clock, statsEngine, next) 84 | 85 | an[IllegalArgumentException] should be thrownBy action.execute(session) 86 | } 87 | 88 | it should "throw an IAE when the table name cannot be validated" in { 89 | val action = JdbcCreateTableAction("request", "${exc_table}", Seq(column(name("foo"), dataType("INTEGER"), constraint("PRIMARY KEY"))), clock, statsEngine, next) 90 | 91 | an[IllegalArgumentException] should be thrownBy action.execute(session) 92 | } 93 | 94 | it should "pass the session to the next action" in { 95 | val nextAction = NextAction(session) 96 | val action = JdbcCreateTableAction("request", "next_table", Seq(column(name("foo"), dataType("INTEGER"), constraint("PRIMARY KEY"))), clock, statsEngine, nextAction) 97 | 98 | action.execute(session) 99 | 100 | waitForLatch(nextAction) 101 | nextAction.called should be(true) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/test/scala/dev/code_n_roll/gatling/jdbc/action/JdbcSelectActionSpec.scala: -------------------------------------------------------------------------------- 1 | package dev.code_n_roll.gatling.jdbc.action 2 | 3 | import dev.code_n_roll.gatling.jdbc.JdbcCheck 4 | import io.gatling.commons.stats.{KO, OK} 5 | import io.gatling.core.Predef._ 6 | import io.gatling.core.stats.writer.ResponseMessage 7 | import scalikejdbc._ 8 | import dev.code_n_roll.gatling.jdbc.Predef._ 9 | import io.gatling.commons.util.DefaultClock 10 | 11 | /** 12 | * Created by ronny on 15.05.17. 13 | */ 14 | class JdbcSelectActionSpec extends JdbcActionSpec { 15 | 16 | private val clock = new DefaultClock 17 | 18 | "JdbcSelectAction" should "use the request name in the log message" in { 19 | DB autoCommit { implicit session => 20 | sql"""CREATE TABLE test_table(id INTEGER PRIMARY KEY );""".execute().apply() 21 | } 22 | val requestName = "simulation" 23 | val latchAction = BlockingLatchAction() 24 | val action = JdbcSelectAction(requestName, "*", "test_table", None, List.empty, _.toMap(), clock, statsEngine, latchAction) 25 | 26 | action.execute(session) 27 | 28 | waitForLatch(latchAction) 29 | statsEngine.dataWriterMsg should have length 1 30 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].name should equal(requestName) 31 | } 32 | 33 | it should "add the exception message to the log message" in { 34 | val latchAction = BlockingLatchAction() 35 | val action = JdbcSelectAction("simulation", "*", "non-existing-table", None, List.empty, _.toMap(), clock, statsEngine, latchAction) 36 | 37 | action.execute(session) 38 | 39 | waitForLatch(latchAction) 40 | statsEngine.dataWriterMsg should have length 1 41 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].message shouldNot be(empty) 42 | } 43 | 44 | it should "select all values without where clause" in { 45 | DB autoCommit { implicit session => 46 | sql"""CREATE TABLE selection(id INTEGER PRIMARY KEY ); INSERT INTO SELECTION VALUES (1);INSERT INTO SELECTION VALUES (2)""".execute().apply() 47 | } 48 | val latchAction = BlockingLatchAction() 49 | val action = JdbcSelectAction("request", "*", "SELECTION", None, List(simpleCheck(list => list.length == 2)), _.toMap(), clock, statsEngine, latchAction) 50 | 51 | action.execute(session) 52 | 53 | waitForLatch(latchAction) 54 | statsEngine.dataWriterMsg should have length 1 55 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(OK) 56 | } 57 | 58 | it should "select values specified by where clause" in { 59 | DB autoCommit { implicit session => 60 | sql"""CREATE TABLE limited(id INTEGER PRIMARY KEY ); INSERT INTO LIMITED VALUES (1);INSERT INTO LIMITED VALUES (2)""".execute().apply() 61 | } 62 | val latchAction = BlockingLatchAction() 63 | val action = JdbcSelectAction("request", "*", "LIMITED", Some("id=2"), List(simpleCheck(list => list.length == 1)), _.toMap(), clock, statsEngine, latchAction) 64 | 65 | action.execute(session) 66 | 67 | waitForLatch(latchAction) 68 | statsEngine.dataWriterMsg should have length 1 69 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(OK) 70 | } 71 | 72 | it should "log an OK value after successful selection" in { 73 | DB autoCommit { implicit session => 74 | sql"""CREATE TABLE success(id INTEGER PRIMARY KEY )""".execute().apply() 75 | } 76 | val latchAction = BlockingLatchAction() 77 | val action = JdbcSelectAction("request", "*", "SUCCESS", None, List.empty, _.toMap(), clock, statsEngine, latchAction) 78 | 79 | action.execute(session) 80 | 81 | waitForLatch(latchAction) 82 | statsEngine.dataWriterMsg should have length 1 83 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(OK) 84 | } 85 | 86 | it should "log an KO value after unsuccessful selection" in { 87 | val latchAction = BlockingLatchAction() 88 | val action = JdbcSelectAction("request", "*", "failure", None, List.empty, _.toMap(), clock, statsEngine, latchAction) 89 | 90 | action.execute(session) 91 | 92 | waitForLatch(latchAction) 93 | statsEngine.dataWriterMsg should have length 1 94 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(KO) 95 | } 96 | 97 | it should "log a KO value if a check fails" in { 98 | DB autoCommit { implicit session => 99 | sql"""CREATE TABLE checkTable(id INTEGER PRIMARY KEY )""".execute().apply() 100 | } 101 | val latchAction = BlockingLatchAction() 102 | val action = JdbcSelectAction("request", "*", "CHECKTABLE", None, List(simpleCheck(_ => false)), _.toMap(), clock, statsEngine, latchAction) 103 | 104 | action.execute(session) 105 | 106 | waitForLatch(latchAction) 107 | statsEngine.dataWriterMsg should have length 1 108 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(KO) 109 | } 110 | 111 | it should "log a OK value if a check is successful" in { 112 | DB autoCommit { implicit session => 113 | sql"""CREATE TABLE check_again(id INTEGER PRIMARY KEY )""".execute().apply() 114 | } 115 | val latchAction = BlockingLatchAction() 116 | val action = JdbcSelectAction("request", "*", "CHECK_AGAIN", None, List(simpleCheck(_ => true)), _.toMap(), clock, statsEngine, latchAction) 117 | 118 | action.execute(session) 119 | 120 | waitForLatch(latchAction) 121 | statsEngine.dataWriterMsg should have length 1 122 | statsEngine.dataWriterMsg.head(session).toOption.get.asInstanceOf[ResponseMessage].status should equal(OK) 123 | } 124 | 125 | it should "throw an IAE when it cannot evaluate the what expression" in { 126 | val action = JdbcSelectAction("request", "${what}", "table", None, List(simpleCheck(_ => true)), _.toMap(), clock, statsEngine, next) 127 | 128 | an[IllegalArgumentException] should be thrownBy action.execute(session) 129 | } 130 | 131 | it should "throw an IAE when it cannot evaluate the from expression" in { 132 | val action = JdbcSelectAction("request", "*", "${from}", None, List(simpleCheck(_ => true)), _.toMap(), clock, statsEngine, next) 133 | 134 | an[IllegalArgumentException] should be thrownBy action.execute(session) 135 | } 136 | 137 | it should "throw an IAE when it cannot evaluate the where expression" in { 138 | val action = JdbcSelectAction("request", "*", "table", Some("${where}"), List(simpleCheck(_ => true)), _.toMap(), clock, statsEngine, next) 139 | 140 | an[IllegalArgumentException] should be thrownBy action.execute(session) 141 | } 142 | 143 | it should "pass the session to the next action" in { 144 | DB autoCommit { implicit session => 145 | sql"""CREATE TABLE insert_next(id INTEGER PRIMARY KEY )""".execute().apply() 146 | } 147 | val nextAction = NextAction(session) 148 | val action = JdbcSelectAction("request", "*", "INSERT_NEXT", None, List(simpleCheck(_ => true)), _.toMap(), clock, statsEngine, nextAction) 149 | 150 | action.execute(session) 151 | 152 | waitForLatch(nextAction) 153 | nextAction.called should be(true) 154 | } 155 | 156 | it should "pass the session to the next action even when a check crashes" in { 157 | DB autoCommit { implicit session => 158 | sql"""CREATE TABLE crashes(id INTEGER PRIMARY KEY )""".execute().apply() 159 | } 160 | val nextAction = NextAction(session.markAsFailed) 161 | val action = JdbcSelectAction("request", "*", "CRASHES", None, List(simpleCheck(_ => throw new RuntimeException("Test error"))), _.toMap(), clock, statsEngine, nextAction) 162 | 163 | action.execute(session) 164 | 165 | waitForLatch(nextAction) 166 | nextAction.called should be(true) 167 | } 168 | 169 | it should "apply the map function to the selection and store it in the session" in { 170 | DB autoCommit { implicit session => 171 | sql"""CREATE TABLE mapping(id INTEGER PRIMARY KEY ); INSERT INTO mapping VALUES (1);""".execute().apply() 172 | } 173 | val nextAction = new BlockingLatchAction() { 174 | override def execute(session: Session): Unit = { 175 | session("value").as[Mapping] shouldEqual Mapping(1) 176 | super.execute(session) 177 | } 178 | } 179 | val action: JdbcSelectAction[Mapping] = JdbcSelectAction( 180 | "request", 181 | "*", 182 | "MAPPING", 183 | None, 184 | List(singleResponse[Mapping].is(Mapping(1)).saveAs("value")), 185 | rs => Mapping(rs.int("id")), 186 | clock, 187 | statsEngine, 188 | nextAction) 189 | 190 | action.execute(session) 191 | 192 | waitForLatch(nextAction) 193 | } 194 | 195 | case class Mapping(id: Int) 196 | 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gatling JDBC Extension 2 | JDBC support for Gatling 3 | 4 | [![Build Status](https://travis-ci.org/rbraeunlich/gatling-jdbc.svg?branch=master)](https://travis-ci.org/rbraeunlich/gatling-jdbc) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.code_n_roll.gatling/jdbc-gatling_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.code_n_roll.gatling/jdbc-gatling_2.12) 6 | 7 | 8 | The JDBC extension for Gatling was originally created to accompany a blog post that shows how to extend Gatling. 9 | Currently, five SQL operations are being supported. See below for the usage. 10 | 11 | ## :exclamation: Attention :exclamation: 12 | 13 | In order to avoid conflicts with `io.gatling:gatling-jdbc` the artifact name has been changed with version 2.0.1. 14 | Instead of `gatling-jdbc` it is now called `jdbc-gatling` (see issue #8). Apart from this, nothing changes. All package names etc. stayed the same. 15 | 16 | Also, by forking it from it's original position the group id and the packages have changed! 17 | The correct import is now `dev.code_n_roll.gatling....` and the group id changed to `dev.code-n-roll.gatling`. 18 | 19 | ## Usage 20 | 21 | ```scala 22 | libraryDependencies += "dev.code_n_roll.gatling" %% "gatling-jdbc" % "version" 23 | ``` 24 | 25 | ### General 26 | 27 | In order to use the JDBC functionality, your simulation has to import `dev.code_n_roll.gatling.jdbc.Predef._`. 28 | The JDBC configuration is done via `jdbc`, e.g.: 29 | 30 | ```scala 31 | val jdbcConfig = jdbc 32 | .url("jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE") 33 | .username("sa") 34 | .password("sa") 35 | .driver("org.h2.Driver") 36 | ``` 37 | Those are currently all the options that can be provided and that have to be provided. 38 | 39 | The entry point for the operations is the `jdbc()` method. The method itself takes a request name as parameter. This name will appear in the reports to represent the operation that follows. 40 | 41 | ### CREATE TABLE 42 | 43 | Creating a table is done via `jdbc().create()`. In order to ease the creation of arbitrarily many columns, the helper class ` 44 | import dev.code_n_roll.gatling.jdbc.builder.column.ColumnHelper._` was created. It is recommended to use it. The datatype of every column has to be provided as a string. 45 | Additionally, constraints are optional, but also have to be passed as strings. E.g.: 46 | ```scala 47 | scenario("createTable"). 48 | exec(jdbc("bar table") 49 | .create() 50 | .table("bar") 51 | .columns( 52 | column( 53 | name("abc"), 54 | dataType("INTEGER"), 55 | constraint("PRIMARY KEY") 56 | ), 57 | column( 58 | name("ac"), 59 | dataType("INTEGER") 60 | ) 61 | ) 62 | ) 63 | ``` 64 | 65 | ### INSERT 66 | 67 | Insertion is done via `jdbc().insert()`. For where to insert, two options are possible. Suppose you have the table from the example above. You can insert the values either by relying on the indices: 68 | ```scala 69 | exec(jdbc("insertion") 70 | .insert() 71 | .into("bar") 72 | .values("${n}, ${n}") 73 | ) 74 | ``` 75 | or by using the column names: 76 | ```scala 77 | exec(jdbc("insertion") 78 | .insert() 79 | .into("bar (abc, ac)") 80 | .values("${n}, ${n}") 81 | ) 82 | ``` 83 | 84 | ### SELECT 85 | 86 | In contrast to the previous operations, select directly requires a parameter and is called via `jdbc().select()`. The intention is to closely resemble the SQL syntax. 87 | Using `where()` is optional for SELECT. Therefore, the following two ways are both valid: 88 | ```scala 89 | exec(jdbc("selection") 90 | .select("*") 91 | .from("bar") 92 | ) 93 | ``` 94 | and 95 | ```scala 96 | exec(jdbc("selection") 97 | .select("*") 98 | .from("bar") 99 | .where("abc=4") 100 | ) 101 | ``` 102 | Of course, as parameter to `select()`, every column or combination of columns can be used, as with basic SQL. 103 | 104 | ### DELETE 105 | 106 | Deletion starts from `jdbc().delete()`. Here, the where clause is optional again. In order to delete certain values the following works: 107 | ```scala 108 | repeat(5, "n"){ 109 | exec(jdbc("deletion") 110 | .delete() 111 | .from("bar") 112 | .where("abc=${n}") 113 | ) 114 | } 115 | ``` 116 | Alternatively, in order to delete everything: 117 | ```scala 118 | exec(jdbc("deletion") 119 | .delete() 120 | .from("bar") 121 | ) 122 | ``` 123 | Please be careful, since no additional validation is being performed and you might lose some data. 124 | 125 | ### DROP TABLE 126 | 127 | The last operation that is being supported is DROP TABLE via `jdbc().drop()` The method only takes a single parameter, the table name. Please be careful again, which table you drop. 128 | Dropping the "bar" table from the first example can be done in the following way: 129 | ```scala 130 | jdbc("drop bar table").drop().table("bar") 131 | ``` 132 | 133 | ### Checks 134 | 135 | Currently, checks are only implemented for SELECT. When importing `dev.code_n_roll.gatling.jdbc.Predef._` two types of checks are provided. 136 | The first type is the SimpleCheck. 137 | 138 | #### SimpleCheck 139 | 140 | The `simpleCheck` method (importet via `Predef`) allows for very basic checks. This method takes a function from `List[Map[String, Any]]` to `Boolean`. 141 | Each element in the list represents a row and the map the individual columns. Checks are simply appended to the selection, e.g.: 142 | ```scala 143 | exec(jdbc("selection") 144 | .select("*") 145 | .from("bar") 146 | .where("abc=4") 147 | .check(simpleCheck(result => result.head("FOO") == 4)) 148 | ) 149 | ``` 150 | A SELECT without a WHERE clause can also be validated with a `simpleCheck`. 151 | 152 | There is also another type of check that is more closely integrated with Gatling, the `CheckBuilders`. 153 | 154 | #### CheckBuilder 155 | 156 | `CheckBuilder` is actually a class provided by Gatling. Based on the Gatling classes, Gatling JDBC provides two types of them. 157 | The two types are basically `JdbcSingeTCheck` and `JdbcManyTCheck`. As the names suggest, both options allow to use the concrete types of the expected response. 158 | To use them, you have to use the methods `singleResponse[T]` or `manyResponse[T]`. 159 | For compatibility reasons there are also the fields `jdbcSingleResponse` and `jdbcManyResponse` which set the type to a `Map[String, Any]`. 160 | The methods and the fields are in scope when importing the complete Predef objects. 161 | 162 | The difference between `singleResponse` and `manyResponse` is that the former extracts the head out of the list of results. So you can only verify a single object. 163 | Whereas the many response, like the simple checks, a `List[T]` is returned. Validation is performed via the Gatling API. 164 | Checking a single result in the untyped way can look like this: 165 | ```scala 166 | exec(jdbc("selectionSingleCheck") 167 | .select("*") 168 | .from("bar") 169 | .where("abc=4") 170 | .check(jdbcSingleResponse.is(("ABC" -> 4, "FOO" -> 4))) 171 | ) 172 | ``` 173 | This validates the data in the two columns "ABC" and "FOO". Please note explicit typing of the map. Without it the compiler will complain. 174 | If you want to use the typed API, you have to provide a function that defines the mapping: 175 | ```scala 176 | case class Stored(abc: Int, foo: Int) 177 | ... 178 | exec(jdbc("selectionSingleCheck") 179 | .select("*") 180 | .from("bar") 181 | .where("abc=4") 182 | .mapResult(rs => Stored(rs.int("abc"), rs.int("foo"))) 183 | .check(singleResponse[Stored].is(Stored(4, 4)) 184 | ).pause(1) 185 | 186 | ``` 187 | For checking multiple results we can again use the deprecated, untyped way: 188 | ```scala 189 | exec(jdbc("selectionManyCheck") 190 | .select("*") 191 | .from("bar") 192 | .where("abc=4 OR abc=5") 193 | .check(jdbcManyResponse.is(List( 194 | Map("ABC" -> 4, "FOO" -> 4), 195 | Map("ABC" -> 5, "FOO" -> 5))) 196 | ) 197 | ) 198 | ``` 199 | Or use the typed API: 200 | ```scala 201 | case class Stored(abc: Int, foo: Int) 202 | ... 203 | exec(jdbc("selectionManyCheck") 204 | .select("*") 205 | .from("bar") 206 | .where("abc=4 OR abc=5") 207 | .mapResult(rs => Stored(rs.int("abc"), rs.int("foo"))) 208 | .check(manyResponse[Stored].is(List( 209 | Stored(4, 4), 210 | Stored(5, 5))) 211 | ) 212 | ``` 213 | Please note that the map function defines the mapping for a single result. You don't have to map the result set into a List of your objects. 214 | 215 | The advantage those CheckBuilder provide is that they can access certain functionality provided by the Gatling interfaces and classes they extend. 216 | The most important one is the possibility to save the result of a selection to the current session. 217 | 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: 218 | ```scala 219 | exec(jdbc("selectionSingleCheckSaving") 220 | .select("*") 221 | .from("bar") 222 | .where("abc=4") 223 | .check(singleResponse.is(("ABC" -> 4, "FOO" -> 4)) 224 | .saveAs("myResult")) 225 | ) 226 | ``` 227 | 228 | ### Final 229 | 230 | Covering all SQL operations is a lot of work and some special commands might not be required for performance tests. 231 | Please keep in mind that the state of this Gatling extension can be considered experimental. Feel free to leave comments and create pull requests. 232 | 233 | ## Publishing 234 | 235 | Firstly, you gotta have in your home `.sbt/1.0/sonatype.sbt` configured to contain your username and password for Sonatype. 236 | Secondly, open the sbt shell an perform the following steps: 237 | 1. `set pgpSecretRing := file("/home//.sbt/gpg/secring.asc")` or where ever it is 238 | 2. `release` 239 | 240 | ## Executing the intergration tests 241 | 242 | If you have to run Docker on your machine as sudo, then to execute the integration tests, sbt has to be started as sudo, too. 243 | Only `sudo sbt gatling:test` will then be allowed to start the container of the databases. 244 | 245 | ## Acknowledgements 246 | 247 | I'd like to thank my former employer [codecentric](https://github.com/codecentric) for providing me the time and space to 248 | get started on this project and transform it to a publishable library. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------