Skip to content
Permalink
Browse files

Basic implementation of action monad and asynchronous execution engine:

- Query, insert, update, delete and schema actions implemented in
  JdbcProfile.

- Query, insert and schema actions implemented in MemoryProfile.

- Query actions implemented in DistributedDriver. This is not an
  optimized implementation based on non-blocking primitives. We simply
  wrap the existing blocking calls and run them inside of
  Future(blocking(...)) in the global ExecutionContext.
  (DistributedDriver in general is not optimized, and is considered
  experimental at this point.)

- As an alternative to TestkitTest, the new AsyncTest class supports
  test methods that return Futures or Actions, and provides the
  necessary infrastructure for such tests. Testkit enforces that test
  methods in TestkitTest subclasses return Unit and that test methods
  in AsyncTest subclasses return Action or Future.

- Further simplifications for Testkit tests:
  - A string interpolator for generating unique names
  - A custom matcher instead of Assert.assertEquals

- Rename `.ddl` extension method for `TableQuery` to `.schema`

- The new API moves some operations to different profile levels:
  RelationalProfile is required for schema-related features and inserts,
  JdbcProfile is required for transactions, updates and deletes.
  BasicProfile only supports querying.

- DistributedProfile only extends BasicProfile, not RelationalProfile.
  The extra operations of RelationalProfile have never been supported
  anyway.

- Upgrade to Xerial-SQLite 3.8.7 to get in-memory databases with shared
  cache across multiple threads. This is needed for some new test cases.
  Disabling the test for multi-column primary keys on SQLite for now in
  ModelBuilderTest. This feature is broken in 3.8.7.

Still missing in this version:

- Pinned sessions

- Wrapping actions for transactions and error handling

- Streaming results

- Get statements from all insert actions
  • Loading branch information
szeiger committed Nov 5, 2014
1 parent 06ee4ed commit 66a9b4100c458272727643f81e886f6546c41706
Showing with 977 additions and 295 deletions.
  1. +1 −1 project/Build.scala
  2. +4 −0 slick-testkit/src/main/resources/testkit-reference.conf
  3. +44 −0 slick-testkit/src/main/scala/com/typesafe/slick/testkit/tests/ActionTest.scala
  4. +0 −22 slick-testkit/src/main/scala/com/typesafe/slick/testkit/tests/ExecutorTest.scala
  5. +9 −4 slick-testkit/src/main/scala/com/typesafe/slick/testkit/tests/ModelBuilderTest.scala
  6. +2 −2 slick-testkit/src/main/scala/com/typesafe/slick/testkit/util/StandardTestDBs.scala
  7. +70 −20 slick-testkit/src/main/scala/com/typesafe/slick/testkit/util/Testkit.scala
  8. +6 −0 slick-testkit/src/main/scala/com/typesafe/slick/testkit/util/TestkitConfig.scala
  9. +68 −0 src/main/scala/scala/slick/action/Action.scala
  10. +49 −9 src/main/scala/scala/slick/backend/DatabaseComponent.scala
  11. +8 −0 src/main/scala/scala/slick/backend/RelationalBackend.scala
  12. +1 −1 src/main/scala/scala/slick/driver/DerbyDriver.scala
  13. +263 −0 src/main/scala/scala/slick/driver/JdbcActionComponent.scala
  14. +1 −1 src/main/scala/scala/slick/driver/JdbcInsertInvokerComponent.scala
  15. +24 −6 src/main/scala/scala/slick/driver/JdbcProfile.scala
  16. +1 −1 src/main/scala/scala/slick/driver/PostgresDriver.scala
  17. +1 −1 src/main/scala/scala/slick/driver/SQLiteDriver.scala
  18. +12 −140 src/main/scala/scala/slick/jdbc/JdbcBackend.scala
  19. +140 −0 src/main/scala/scala/slick/jdbc/LoggingStatement.scala
  20. +5 −0 src/main/scala/scala/slick/lifted/Aliases.scala
  21. +1 −1 src/main/scala/scala/slick/lifted/ExtensionMethods.scala
  22. +1 −1 src/main/scala/scala/slick/lifted/Rep.scala
  23. +8 −5 src/main/scala/scala/slick/memory/DistributedBackend.scala
  24. +17 −3 src/main/scala/scala/slick/memory/DistributedProfile.scala
  25. +17 −9 src/main/scala/scala/slick/memory/HeapBackend.scala
  26. +66 −7 src/main/scala/scala/slick/memory/MemoryProfile.scala
  27. +11 −21 src/main/scala/scala/slick/memory/MemoryQueryingProfile.scala
  28. +57 −20 src/main/scala/scala/slick/profile/BasicProfile.scala
  29. +75 −17 src/main/scala/scala/slick/profile/RelationalProfile.scala
  30. +13 −1 src/main/scala/scala/slick/profile/SqlProfile.scala
  31. +1 −1 src/main/scala/scala/slick/util/AsyncExecutor.scala
  32. +1 −1 src/sphinx/upgrade.rst
@@ -32,7 +32,7 @@ object SlickBuild extends Build {
val h2 = "com.h2database" % "h2" % "1.3.170"
val testDBs = Seq(
h2,
"org.xerial" % "sqlite-jdbc" % "3.7.2",
"org.xerial" % "sqlite-jdbc" % "3.8.7",
"org.apache.derby" % "derby" % "10.9.1.0",
"org.hsqldb" % "hsqldb" % "2.2.8"
)
@@ -11,9 +11,13 @@ testkit {
# absTestDir is computed from this and injected here for use in substitutions
testDir = test-dbs

# The duration after which asynchronous tests should be aborted and failed
asyncTimeout = 1 minutes

# All TestkitTest classes to run
testPackage = com.typesafe.slick.testkit.tests
testClasses = [
${testPackage}.ActionTest
${testPackage}.AggregateTest
${testPackage}.ColumnDefaultTest
${testPackage}.CountTest
@@ -0,0 +1,44 @@
package com.typesafe.slick.testkit.tests

import com.typesafe.slick.testkit.util.{RelationalTestDB, AsyncTest}

import scala.concurrent.Future

class ActionTest extends AsyncTest[RelationalTestDB] {
import tdb.profile.api._

def testSimpleActionAsFuture = {
class T(tag: Tag) extends Table[Int](tag, u"t") {
def a = column[Int]("a")
def * = a
}
val ts = TableQuery[T]

for {
_ <- db.run {
ts.ddl.create >>
(ts ++= Seq(2, 3, 1, 5, 4))
}
q1 = ts.sortBy(_.a).map(_.a)
f1 = db.run(q1.result)
r1 <- f1 : Future[Seq[Int]]
_ = r1 shouldBe List(1, 2, 3, 4, 5)
} yield ()
}

def testSimpleActionAsAction = {
class T(tag: Tag) extends Table[Int](tag, u"t") {
def a = column[Int]("a")
def * = a
}
val ts = TableQuery[T]

for {
_ <- ts.schema.create
_ <- ts ++= Seq(2, 3, 1, 5, 4)
q1 = ts.sortBy(_.a).map(_.a)
r1 <- q1.result
_ = (r1 : Seq[Int]) shouldBe List(1, 2, 3, 4, 5)
} yield ()
}
}
@@ -1,13 +1,8 @@
package com.typesafe.slick.testkit.tests

import java.util.concurrent.TimeUnit

import org.junit.Assert._
import com.typesafe.slick.testkit.util.{RelationalTestDB, TestkitTest}

import scala.concurrent.Await
import scala.concurrent.duration.Duration

class ExecutorTest extends TestkitTest[RelationalTestDB] {
import tdb.profile.simple._
override val reuseInstance = true
@@ -76,21 +71,4 @@ class ExecutorTest extends TestkitTest[RelationalTestDB] {
val r3b = ts.to[Array].map(_.a).run
assertTrue(r3b.isInstanceOf[Array[Int]])
}

def testAsyncExecution {
class T(tag: Tag) extends Table[Int](tag, "t_async") {
def a = column[Int]("a")
def * = a
}
val ts = TableQuery[T]

val f1 = db.runAsync { implicit implicitSession =>
ts.ddl.create
ts ++= Seq(2, 3, 1, 5, 4)
val q1 = ts.sortBy(_.a).map(_.a)
q1.run
}
val r1 = Await.result(f1, Duration(1, TimeUnit.MINUTES))
assertEquals(List(1, 2, 3, 4, 5), r1)
}
}
@@ -1,6 +1,7 @@
package com.typesafe.slick.testkit.tests

import org.junit.Assert._
import scala.slick.driver.SQLiteDriver
import scala.slick.model._
import scala.slick.ast.ColumnOption
import scala.slick.jdbc.meta.MTable
@@ -10,7 +11,7 @@ import com.typesafe.slick.testkit.util.{JdbcTestDB, TestkitTest}
class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
import tdb.profile.simple._

def test { ifCap(jcap.createModel){
def test: Unit = ifCap(jcap.createModel) {
class Categories(tag: Tag) extends Table[(Int, String)](tag, "categories") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name", O.DBType("VARCHAR(123)"))
@@ -176,7 +177,12 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
val posts = model.tables.filter(_.name.table.toUpperCase=="POSTS").head
assertEquals( 5, posts.columns.size )
assertEquals( posts.indices.toString, 0, posts.indices.size )
assertEquals( 2, posts.primaryKey.get.columns.size )
if(tdb.driver != SQLiteDriver) {
// Reporting of multi-column primary keys through JDBC metadata is broken in Xerial SQLite 3.8:
// https://bitbucket.org/xerial/sqlite-jdbc/issue/107/databasemetadatagetprimarykeys-does-not
assertEquals( Some(2), posts.primaryKey.map(_.columns.size) )
assert( !posts.columns.exists(_.options.exists(_ == ColumnOption.PrimaryKey)) )
}
assertEquals( 1, posts.foreignKeys.size )
if(tdb.profile != slick.driver.SQLiteDriver){
assertEquals( "CATEGORY_FK", posts.foreignKeys.head.name.get.toUpperCase )
@@ -208,7 +214,6 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
posts.columns.filter(_.name == "some_string").head
.options.collect{case ColumnOption.Length(length,varying) => (length,varying)}.head
)
assert( !posts.columns.exists(_.options.exists(_ == ColumnOption.PrimaryKey)) )
posts.columns.foreach{
_.options.foreach{
case ColumnOption.Length(length,varying) => length < 256
@@ -378,5 +383,5 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
assertEquals( None, noDefaultTest.map(_.stringOption).first )
}
}
}}
}
}
@@ -3,6 +3,7 @@ package com.typesafe.slick.testkit.util
import java.io.File
import java.util.logging.{Level, Logger}
import java.sql.SQLException
import scala.concurrent.ExecutionContext
import scala.slick.driver._
import scala.slick.memory.MemoryDriver
import scala.slick.jdbc.{ResultSetInvoker, StaticQuery => Q}
@@ -44,9 +45,8 @@ object StandardTestDBs {
}
}

lazy val SQLiteMem = new SQLiteTestDB("jdbc:sqlite::memory:", "sqlitemem") {
lazy val SQLiteMem = new SQLiteTestDB("jdbc:sqlite:file:slick_test?mode=memory&cache=shared", "sqlitemem") {
override def isPersistent = false
override def isShared = false
}

lazy val SQLiteDisk = {
@@ -1,14 +1,23 @@
package com.typesafe.slick.testkit.util

import java.util.concurrent.atomic.AtomicInteger

import scala.language.existentials

import scala.concurrent.{ExecutionContext, Await, Future}
import scala.reflect.ClassTag
import scala.slick.action.{Effect, Action}
import scala.util.control.NonFatal

import java.lang.reflect.Method
import java.util.concurrent.{ExecutionException, TimeUnit}

import org.junit.runner.Description
import org.junit.runner.notification.RunNotifier
import org.junit.runners.model._
import org.junit.Assert._
import org.junit.Assert
import scala.slick.profile.{RelationalProfile, SqlProfile, Capability}
import scala.slick.driver.JdbcProfile
import java.lang.reflect.Method
import scala.reflect.ClassTag

/** JUnit runner for the Slick driver test kit. */
class Testkit(clazz: Class[_ <: DriverTest], runnerBuilder: RunnerBuilder) extends SimpleParentRunner[TestMethod](clazz) {
@@ -36,7 +45,7 @@ class Testkit(clazz: Class[_ <: DriverTest], runnerBuilder: RunnerBuilder) exten
val is = children.iterator.map(ch => (ch, ch.cl.newInstance()))
.filter{ case (_, to) => to.setTestDB(tdb) }.zipWithIndex.toIndexedSeq
val last = is.length - 1
var previousTestObject: TestkitTest[_ >: Null <: TestDB] = null
var previousTestObject: GenericTest[_ >: Null <: TestDB] = null
for(((ch, preparedTestObject), idx) <- is) {
val desc = describeChild(ch)
notifier.fireTestStarted(desc)
@@ -45,9 +54,7 @@ class Testkit(clazz: Class[_ <: DriverTest], runnerBuilder: RunnerBuilder) exten
if(previousTestObject ne null) previousTestObject
else preparedTestObject
previousTestObject = null
try {
ch.method.invoke(testObject)
} finally {
try ch.run(testObject) finally {
val skipCleanup = idx == last || (testObject.reuseInstance && (ch.cl eq is(idx+1)._1._1.cl))
if(skipCleanup) {
if(idx == last) testObject.closeKeepAlive()
@@ -67,9 +74,27 @@ abstract class DriverTest(val tdb: TestDB) {
def tests = TestkitConfig.testClasses
}

case class TestMethod(name: String, desc: Description, method: Method, cl: Class[_ <: TestkitTest[_ >: Null <: TestDB]])
case class TestMethod(name: String, desc: Description, method: Method, cl: Class[_ <: GenericTest[_ >: Null <: TestDB]]) {
private[this] def await[T](f: Future[T]): T =
try Await.result(f, TestkitConfig.asyncTimeout)
catch { case ex: ExecutionException => throw ex.getCause }

def run(testObject: GenericTest[_]): Unit = {
val r = method.getReturnType
testObject match {
case testObject: TestkitTest[_] =>
if(r == Void.TYPE) method.invoke(testObject)
else throw new RuntimeException(s"Illegal return type: '${r.getName}' in test method '$name' -- TestkitTest methods must return Unit")

case testObject: AsyncTest[_] =>
if(r == classOf[Future[_]]) await(method.invoke(testObject).asInstanceOf[Future[Any]])
else if(r == classOf[Action[_, _]]) await(testObject.db.run(method.invoke(testObject).asInstanceOf[Action[Effect, Any]]))
else throw new RuntimeException(s"Illegal return type: '${r.getName}' in test method '$name' -- AsyncTest methods must return Future or Action")
}
}
}

abstract class TestkitTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TDB]) {
sealed abstract class GenericTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TDB]) {
protected[this] var _tdb: TDB = null
private[testkit] def setTestDB(tdb: TestDB): Boolean = {
tdb match {
@@ -80,18 +105,11 @@ abstract class TestkitTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TD
false
}
}
//lazy val tdb: JdbcTestDB = _tdb.asInstanceOf[JdbcTestDB]
lazy val tdb: TDB = _tdb

private[this] var keepAliveSession: tdb.profile.Backend#Session = null

@deprecated("Use implicitSession instead of sharedSession", "2.2")
protected final def sharedSession: tdb.profile.Backend#Session = implicitSession
private[testkit] var keepAliveSession: tdb.profile.Backend#Session = null

protected implicit def implicitSession: tdb.profile.Backend#Session = {
db
keepAliveSession
}
private[this] var unique = new AtomicInteger

val reuseInstance = false

@@ -122,11 +140,27 @@ abstract class TestkitTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TD
} catch {
case e: Exception if !scala.util.control.Exception.shouldRethrow(e) =>
}
if(succeeded) fail("Exception expected")
if(succeeded) Assert.fail("Exception expected")
}

def assertAllMatch[T](t: TraversableOnce[T])(f: PartialFunction[T, _]) = t.foreach { x =>
if(!f.isDefinedAt(x)) fail("Expected shape not matched by: "+x)
if(!f.isDefinedAt(x)) Assert.fail("Expected shape not matched by: "+x)
}

implicit class AssertionExtensionMethods(v: Any) {
private[this] val cln = getClass.getName
private[this] def fixStack(f: => Unit): Unit = try f catch {
case ex: AssertionError =>
ex.setStackTrace(ex.getStackTrace.iterator.filterNot(_.getClassName.startsWith(cln)).toArray)
throw ex
}

def shouldBe (o: Any): Unit = fixStack(Assert.assertEquals(o, v))
}

implicit class StringContextExtensionMethods(s: StringContext) {
/** Generate a unique name suitable for a database entity */
def u(args: Any*) = s.standardInterpolator(identity, args) + "_" + unique.incrementAndGet()
}

def rcap = RelationalProfile.capabilities
@@ -138,3 +172,19 @@ abstract class TestkitTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TD
def ifNotCap[T](caps: Capability*)(f: => T): Unit =
if(!caps.forall(c => tdb.capabilities.contains(c))) f
}

abstract class TestkitTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TDB]) extends GenericTest[TDB] {
@deprecated("Use implicitSession instead of sharedSession", "2.2")
protected final def sharedSession: tdb.profile.Backend#Session = implicitSession

protected implicit def implicitSession: tdb.profile.Backend#Session = {
db
keepAliveSession
}
}

abstract class AsyncTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TDB]) extends GenericTest[TDB] {
final override val reuseInstance = true

protected implicit def asyncTestExecutionContext = ExecutionContext.global
}
@@ -1,9 +1,12 @@
package com.typesafe.slick.testkit.util

import java.util.concurrent.TimeUnit

import com.typesafe.config.{ConfigValueFactory, Config, ConfigFactory}
import java.io.{FileInputStream, File}
import java.util.Properties
import scala.collection.JavaConverters._
import scala.concurrent.duration.Duration
import scala.slick.SlickException

/** Manages the configuration for TestKit tests.
@@ -55,6 +58,9 @@ object TestkitConfig {
getStrings(testkitConfig, "testClasses").getOrElse(Nil).
map(n => Class.forName(n).asInstanceOf[Class[_ <: TestkitTest[_ >: Null <: TestDB]]])

/** The duration after which asynchronous tests should be aborted and failed */
lazy val asyncTimeout = Duration(testkitConfig.getDuration("asyncTimeout", TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS)

def getStrings(config: Config, path: String): Option[Seq[String]] = {
if(config.hasPath(path)) {
config.getValue(path).unwrapped() match {

0 comments on commit 66a9b41

Please sign in to comment.
You can’t perform that action at this time.