Skip to content

Commit

Permalink
Basic implementation of action monad and asynchronous execution engine:
Browse files Browse the repository at this point in the history
- 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 11, 2014
1 parent 06ee4ed commit 66a9b41
Show file tree
Hide file tree
Showing 32 changed files with 977 additions and 295 deletions.
2 changes: 1 addition & 1 deletion project/Build.scala
Expand Up @@ -32,7 +32,7 @@ object SlickBuild extends Build {
val h2 = "com.h2database" % "h2" % "1.3.170" val h2 = "com.h2database" % "h2" % "1.3.170"
val testDBs = Seq( val testDBs = Seq(
h2, h2,
"org.xerial" % "sqlite-jdbc" % "3.7.2", "org.xerial" % "sqlite-jdbc" % "3.8.7",
"org.apache.derby" % "derby" % "10.9.1.0", "org.apache.derby" % "derby" % "10.9.1.0",
"org.hsqldb" % "hsqldb" % "2.2.8" "org.hsqldb" % "hsqldb" % "2.2.8"
) )
Expand Down
4 changes: 4 additions & 0 deletions slick-testkit/src/main/resources/testkit-reference.conf
Expand Up @@ -11,9 +11,13 @@ testkit {
# absTestDir is computed from this and injected here for use in substitutions # absTestDir is computed from this and injected here for use in substitutions
testDir = test-dbs testDir = test-dbs


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

# All TestkitTest classes to run # All TestkitTest classes to run
testPackage = com.typesafe.slick.testkit.tests testPackage = com.typesafe.slick.testkit.tests
testClasses = [ testClasses = [
${testPackage}.ActionTest
${testPackage}.AggregateTest ${testPackage}.AggregateTest
${testPackage}.ColumnDefaultTest ${testPackage}.ColumnDefaultTest
${testPackage}.CountTest ${testPackage}.CountTest
Expand Down
@@ -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 package com.typesafe.slick.testkit.tests


import java.util.concurrent.TimeUnit

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


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

class ExecutorTest extends TestkitTest[RelationalTestDB] { class ExecutorTest extends TestkitTest[RelationalTestDB] {
import tdb.profile.simple._ import tdb.profile.simple._
override val reuseInstance = true override val reuseInstance = true
Expand Down Expand Up @@ -76,21 +71,4 @@ class ExecutorTest extends TestkitTest[RelationalTestDB] {
val r3b = ts.to[Array].map(_.a).run val r3b = ts.to[Array].map(_.a).run
assertTrue(r3b.isInstanceOf[Array[Int]]) 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 package com.typesafe.slick.testkit.tests


import org.junit.Assert._ import org.junit.Assert._
import scala.slick.driver.SQLiteDriver
import scala.slick.model._ import scala.slick.model._
import scala.slick.ast.ColumnOption import scala.slick.ast.ColumnOption
import scala.slick.jdbc.meta.MTable import scala.slick.jdbc.meta.MTable
Expand All @@ -10,7 +11,7 @@ import com.typesafe.slick.testkit.util.{JdbcTestDB, TestkitTest}
class ModelBuilderTest extends TestkitTest[JdbcTestDB] { class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
import tdb.profile.simple._ 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") { class Categories(tag: Tag) extends Table[(Int, String)](tag, "categories") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc) def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name", O.DBType("VARCHAR(123)")) def name = column[String]("name", O.DBType("VARCHAR(123)"))
Expand Down Expand Up @@ -176,7 +177,12 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
val posts = model.tables.filter(_.name.table.toUpperCase=="POSTS").head val posts = model.tables.filter(_.name.table.toUpperCase=="POSTS").head
assertEquals( 5, posts.columns.size ) assertEquals( 5, posts.columns.size )
assertEquals( posts.indices.toString, 0, posts.indices.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 ) assertEquals( 1, posts.foreignKeys.size )
if(tdb.profile != slick.driver.SQLiteDriver){ if(tdb.profile != slick.driver.SQLiteDriver){
assertEquals( "CATEGORY_FK", posts.foreignKeys.head.name.get.toUpperCase ) assertEquals( "CATEGORY_FK", posts.foreignKeys.head.name.get.toUpperCase )
Expand Down Expand Up @@ -208,7 +214,6 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
posts.columns.filter(_.name == "some_string").head posts.columns.filter(_.name == "some_string").head
.options.collect{case ColumnOption.Length(length,varying) => (length,varying)}.head .options.collect{case ColumnOption.Length(length,varying) => (length,varying)}.head
) )
assert( !posts.columns.exists(_.options.exists(_ == ColumnOption.PrimaryKey)) )
posts.columns.foreach{ posts.columns.foreach{
_.options.foreach{ _.options.foreach{
case ColumnOption.Length(length,varying) => length < 256 case ColumnOption.Length(length,varying) => length < 256
Expand Down Expand Up @@ -378,5 +383,5 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
assertEquals( None, noDefaultTest.map(_.stringOption).first ) assertEquals( None, noDefaultTest.map(_.stringOption).first )
} }
} }
}} }
} }
Expand Up @@ -3,6 +3,7 @@ package com.typesafe.slick.testkit.util
import java.io.File import java.io.File
import java.util.logging.{Level, Logger} import java.util.logging.{Level, Logger}
import java.sql.SQLException import java.sql.SQLException
import scala.concurrent.ExecutionContext
import scala.slick.driver._ import scala.slick.driver._
import scala.slick.memory.MemoryDriver import scala.slick.memory.MemoryDriver
import scala.slick.jdbc.{ResultSetInvoker, StaticQuery => Q} import scala.slick.jdbc.{ResultSetInvoker, StaticQuery => Q}
Expand Down Expand Up @@ -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 isPersistent = false
override def isShared = false
} }


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


import java.util.concurrent.atomic.AtomicInteger

import scala.language.existentials 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.Description
import org.junit.runner.notification.RunNotifier import org.junit.runner.notification.RunNotifier
import org.junit.runners.model._ import org.junit.runners.model._
import org.junit.Assert._ import org.junit.Assert
import scala.slick.profile.{RelationalProfile, SqlProfile, Capability} import scala.slick.profile.{RelationalProfile, SqlProfile, Capability}
import scala.slick.driver.JdbcProfile import scala.slick.driver.JdbcProfile
import java.lang.reflect.Method
import scala.reflect.ClassTag


/** JUnit runner for the Slick driver test kit. */ /** JUnit runner for the Slick driver test kit. */
class Testkit(clazz: Class[_ <: DriverTest], runnerBuilder: RunnerBuilder) extends SimpleParentRunner[TestMethod](clazz) { class Testkit(clazz: Class[_ <: DriverTest], runnerBuilder: RunnerBuilder) extends SimpleParentRunner[TestMethod](clazz) {
Expand Down Expand Up @@ -36,7 +45,7 @@ class Testkit(clazz: Class[_ <: DriverTest], runnerBuilder: RunnerBuilder) exten
val is = children.iterator.map(ch => (ch, ch.cl.newInstance())) val is = children.iterator.map(ch => (ch, ch.cl.newInstance()))
.filter{ case (_, to) => to.setTestDB(tdb) }.zipWithIndex.toIndexedSeq .filter{ case (_, to) => to.setTestDB(tdb) }.zipWithIndex.toIndexedSeq
val last = is.length - 1 val last = is.length - 1
var previousTestObject: TestkitTest[_ >: Null <: TestDB] = null var previousTestObject: GenericTest[_ >: Null <: TestDB] = null
for(((ch, preparedTestObject), idx) <- is) { for(((ch, preparedTestObject), idx) <- is) {
val desc = describeChild(ch) val desc = describeChild(ch)
notifier.fireTestStarted(desc) notifier.fireTestStarted(desc)
Expand All @@ -45,9 +54,7 @@ class Testkit(clazz: Class[_ <: DriverTest], runnerBuilder: RunnerBuilder) exten
if(previousTestObject ne null) previousTestObject if(previousTestObject ne null) previousTestObject
else preparedTestObject else preparedTestObject
previousTestObject = null previousTestObject = null
try { try ch.run(testObject) finally {
ch.method.invoke(testObject)
} finally {
val skipCleanup = idx == last || (testObject.reuseInstance && (ch.cl eq is(idx+1)._1._1.cl)) val skipCleanup = idx == last || (testObject.reuseInstance && (ch.cl eq is(idx+1)._1._1.cl))
if(skipCleanup) { if(skipCleanup) {
if(idx == last) testObject.closeKeepAlive() if(idx == last) testObject.closeKeepAlive()
Expand All @@ -67,9 +74,27 @@ abstract class DriverTest(val tdb: TestDB) {
def tests = TestkitConfig.testClasses 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 protected[this] var _tdb: TDB = null
private[testkit] def setTestDB(tdb: TestDB): Boolean = { private[testkit] def setTestDB(tdb: TestDB): Boolean = {
tdb match { tdb match {
Expand All @@ -80,18 +105,11 @@ abstract class TestkitTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TD
false false
} }
} }
//lazy val tdb: JdbcTestDB = _tdb.asInstanceOf[JdbcTestDB]
lazy val tdb: TDB = _tdb lazy val tdb: TDB = _tdb


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

@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 = { private[this] var unique = new AtomicInteger
db
keepAliveSession
}


val reuseInstance = false val reuseInstance = false


Expand Down Expand Up @@ -122,11 +140,27 @@ abstract class TestkitTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TD
} catch { } catch {
case e: Exception if !scala.util.control.Exception.shouldRethrow(e) => 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 => 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 def rcap = RelationalProfile.capabilities
Expand All @@ -138,3 +172,19 @@ abstract class TestkitTest[TDB >: Null <: TestDB](implicit TdbClass: ClassTag[TD
def ifNotCap[T](caps: Capability*)(f: => T): Unit = def ifNotCap[T](caps: Capability*)(f: => T): Unit =
if(!caps.forall(c => tdb.capabilities.contains(c))) f 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 package com.typesafe.slick.testkit.util


import java.util.concurrent.TimeUnit

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


/** Manages the configuration for TestKit tests. /** Manages the configuration for TestKit tests.
Expand Down Expand Up @@ -55,6 +58,9 @@ object TestkitConfig {
getStrings(testkitConfig, "testClasses").getOrElse(Nil). getStrings(testkitConfig, "testClasses").getOrElse(Nil).
map(n => Class.forName(n).asInstanceOf[Class[_ <: TestkitTest[_ >: Null <: TestDB]]]) 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]] = { def getStrings(config: Config, path: String): Option[Seq[String]] = {
if(config.hasPath(path)) { if(config.hasPath(path)) {
config.getValue(path).unwrapped() match { config.getValue(path).unwrapped() match {
Expand Down

0 comments on commit 66a9b41

Please sign in to comment.