Skip to content

Commit

Permalink
Further work on allowiung cucumber to work as either a standalone 'cu…
Browse files Browse the repository at this point in the history
…cumber' task or as part of the built in 'test' task.

- Added a launcher for launching cucumber reflectively using a supplied test class loader
- Refinements to the TestFramework class for implementing into SBT as a standard test library
- The features directory now defaults to being the root of the classpath rather than src/test/features
- Added a new project that demonstrated how to wite up the test integration
- Plugin now provides a group of settings that enable test integration
  • Loading branch information
skipoleschris committed Nov 8, 2012
1 parent 0fbc9a0 commit 0ec12b2
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 24 deletions.
Expand Up @@ -2,6 +2,8 @@ package templemore.sbt.cucumber

import org.scalatools.testing._

import java.util.Properties

/**
* Framework implementation that allows cucumber to be run as part of sbt's
* standard 'test' phase.
Expand All @@ -13,21 +15,48 @@ class CucumberFramework extends Framework {
val tests = Array[Fingerprint](CucumberRunOnceFingerprint)

def testRunner(testClassLoader: ClassLoader, loggers: Array[Logger]) = {
//TODO
println("%%% Creating a new runner...")
new CucumberRunner()
loggers foreach (_.debug("Creating a new Cucumber test runner"))
new CucumberRunner(testClassLoader, loggers)
}
}

class CucumberRunner extends Runner2 {
def run(testClassName: String, fingerprint: Fingerprint, eventHandler: EventHandler, args: Array[String]) = {
println("%%% CUCUMBER RUNNER")
println("%%% Running test class: " + testClassName)
println("%%% with fingerprint: " + fingerprint)
println("%%% with eventHandler: " + eventHandler)
println("%%% with args: " + args.mkString(", "))
//TODO
}
class CucumberRunner(testClassLoader: ClassLoader, loggers: Array[Logger]) extends Runner2 {
private val cucumber = new ReflectingCucumberLauncher(debug = logDebug, error = logError)

def run(testClassName: String, fingerprint: Fingerprint, eventHandler: EventHandler, args: Array[String]) = try {
val arguments = Array("--glue", "", "--format", "pretty", "classpath:")

cucumber(arguments, testClassLoader) match {
case 0 =>
logDebug("Cucumber tests completed successfully")
eventHandler.handle(SuccessEvent(testClassName))
case _ =>
logDebug("Failure while running Cucumber tests")
eventHandler.handle(FailureEvent(testClassName))
}
} catch {
case e => eventHandler.handle(ErrorEvent(testClassName, e))
}

private def logError(message: String) = loggers foreach (_ error message)
private def logDebug(message: String) = loggers foreach (_ debug message)

case class SuccessEvent(testName: String) extends Event {
val description = "Cucumber tests completed successfully."
val result = Result.Success
val error: Throwable = null
}

case class FailureEvent(testName: String) extends Event {
val description = "There were test failures (or undefined/pending steps)."
val result = Result.Failure
val error: Throwable = null
}

case class ErrorEvent(testName: String, error: Throwable) extends Event {
val description = "An error occurred while running Cucumber."
val result = Result.Error
}
}

object CucumberRunOnceFingerprint extends SubclassFingerprint {
Expand Down
@@ -0,0 +1,74 @@
package templemore.sbt.cucumber

import java.lang.reflect.InvocationTargetException
import java.util.Properties

class ReflectingCucumberLauncher(debug: (String) => Unit, error: (String) => Unit) {

private val RuntimeOptionsClassName = "cucumber.runtime.RuntimeOptions"
private val MultiLoaderClassName = "cucumber.runtime.io.MultiLoader"
private val MultiLoaderClassName_1_0_9 = "cucumber.io.MultiLoader"
private val RuntimeClassName = "cucumber.runtime.Runtime"

def apply(cucumberArguments: Array[String],
testClassLoader: ClassLoader): Int = {
debug("Cucumber arguments: " + cucumberArguments.mkString(" "))
val runtime = buildRuntime(System.getProperties, cucumberArguments, testClassLoader)
runCucumber(runtime).asInstanceOf[Byte].intValue
}

private def runCucumber(runtime: AnyRef) = try {
val runtimeClass = runtime.getClass
runtimeClass.getMethod("writeStepdefsJson").invoke(runtime)
runtimeClass.getMethod("run").invoke(runtime)
runtimeClass.getMethod("exitStatus").invoke(runtime)
} catch {
case e: InvocationTargetException => {
val cause = if ( e.getCause == null ) e else e.getCause
error("Error running cucumber. Cause: " + cause.getMessage)
throw cause
}
}

private def buildRuntime(properties: Properties,
arguments: Array[String],
classLoader: ClassLoader): AnyRef = {
def buildLoader(clazz: Class[_]) =
clazz.getConstructor(classOf[ClassLoader]).newInstance(classLoader).asInstanceOf[AnyRef]
def buildOptions(clazz: Class[_]) =
clazz.getConstructor(classOf[Properties], classOf[Array[String]]).newInstance(properties.asInstanceOf[AnyRef], arguments).asInstanceOf[AnyRef]

val (runtimeClass, optionsClass, loaderClass) = loadCucumberClasses(classLoader)
val runtimeConstructor = runtimeClass.getConstructor(loaderClass.getInterfaces()(0), classOf[ClassLoader], optionsClass)
runtimeConstructor.newInstance(buildLoader(loaderClass), classLoader, buildOptions(optionsClass)).asInstanceOf[AnyRef]
}

private def loadCucumberClasses(classLoader: ClassLoader) = try {
val multiLoaderClassName = cucumberVersion(classLoader) match {
case "1.0.9" => MultiLoaderClassName_1_0_9
case _ => MultiLoaderClassName
}

val runtimeOptionsClass = classLoader.loadClass(RuntimeOptionsClassName)
val multiLoaderClass = classLoader.loadClass(multiLoaderClassName)
val runtimeClass = classLoader.loadClass(RuntimeClassName)
(runtimeClass, runtimeOptionsClass, multiLoaderClass)
} catch {
case e: ClassNotFoundException =>
error("Unable to load Cucumber classes. Please check your project dependencied. (Details: " + e.getMessage + ")")
throw e
}

private def cucumberVersion(classLoader: ClassLoader) = {
val stream = classLoader.getResourceAsStream("cucumber/version.properties")
try {
val props = new Properties()
props.load(stream)
val version = props.getProperty("cucumber-jvm.version")
debug("Determined cucumber-jvm version to be: " + version)
version
} finally {
stream.close()
}
}
}
Expand Up @@ -27,7 +27,7 @@ object CucumberPlugin extends Plugin with Integration {
val cucumberJVMOptions = SettingKey[List[String]]("cucumber-jvm-options")

val cucumberMainClass = SettingKey[String]("cucumber-main-class")
val cucumberFeaturesDir = SettingKey[File]("cucumber-features-directory")
val cucumberFeaturesLocation = SettingKey[String]("cucumber-features-location")
val cucumberStepsBasePackage = SettingKey[String]("cucumber-steps-base-package")
val cucumberExtraOptions = SettingKey[List[String]]("cucumber-extra-options")

Expand All @@ -53,8 +53,8 @@ object CucumberPlugin extends Plugin with Integration {
}

protected def cucumberOptionsTask: Initialize[Task[Options]] =
(cucumberFeaturesDir, cucumberStepsBasePackage, cucumberExtraOptions,
cucumberBefore, cucumberAfter) map ((fd, bp, o, bf, af) => Options(fd, bp, o, bf, af))
(cucumberFeaturesLocation, cucumberStepsBasePackage, cucumberExtraOptions,
cucumberBefore, cucumberAfter) map ((fl, bp, o, bf, af) => Options(fl, bp, o, bf, af))

protected def cucumberOutputTask: Initialize[Task[Output]] =
(cucumberPrettyReport, cucumberHtmlReport, cucumberJunitReport, cucumberJsonReport,
Expand Down Expand Up @@ -89,7 +89,8 @@ object CucumberPlugin extends Plugin with Integration {
cucumberJVMOptions := Nil,

cucumberMainClass <<= (scalaVersion) { sv => cucumberMain(sv) },
cucumberFeaturesDir <<= (baseDirectory) { _ / "src" / "test" / "features" },
cucumberFeaturesLocation := "classpath:",
// Replaced with cucumber on the classpath: cucumberFeaturesLocation <<= (baseDirectory) { (_ / "src" / "test" / "features").getPath },
cucumberStepsBasePackage := "",
cucumberExtraOptions := List.empty[String],

Expand Down
Expand Up @@ -62,7 +62,7 @@ trait Integration {
output.options ++
makeOptionsList(tags, "--tags") ++
makeOptionsList(names, "--name") ++
(options.featuresDir.getPath :: Nil)
(options.featuresLocation :: Nil)
JvmLauncher(jvmSettings).launch(cucumberParams)
}
}
4 changes: 2 additions & 2 deletions plugin/src/main/scala/templemore/sbt/cucumber/Options.scala
Expand Up @@ -7,11 +7,11 @@ import java.io.File
*
* @author Chris Turner
*/
case class Options(featuresDir: File,
case class Options(featuresLocation: String,
basePackage: String,
extraOptions: List[String],
beforeFunc: () => Unit,
afterFunc: () => Unit) {

def featuresPresent = featuresDir.exists
def featuresPresent = featuresLocation.startsWith("classpath:") || (new File(featuresLocation).exists)
}
6 changes: 6 additions & 0 deletions testProjects/testIntegrationProject/.gitignore
@@ -0,0 +1,6 @@
.DS_Store
target
project/boot
project/target
project/plugins/target
project/plugins/project
@@ -1,4 +1,4 @@
name := "test-interface-project"
name := "test-project"

version := "0.7.0"

Expand All @@ -7,7 +7,7 @@ organization := "templemore"
scalaVersion := "2.9.2"

libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "1.7.2" % "test",
"org.scalatest" %% "scalatest" % "1.7.2" % "test",
"templemore" %% "sbt-cucumber-integration" % "0.7.0" % "test"
)

Expand Down
@@ -1,3 +1,5 @@
resolvers += "Templemore Repository" at "http://templemore.co.uk/repo"

addSbtPlugin("templemore" % "sbt-cucumber-plugin" % "0.7.0")


Expand Up @@ -20,9 +20,7 @@ class CucumberJarStepDefinitions extends ScalaDsl with EN with ShouldMatchers {
Then("""^Cucumber is executed against the features and step definitions$""") { () =>
givenCalled should be (true)
whenCalled should be (true)
System.getProperty("testing") should be ("true")
System.getProperty("demo") should be ("yes")
}
}

class CucumberSuite extends RunCucumber
class CucumberSuite extends RunCucumber
1 change: 1 addition & 0 deletions testProjects/testProject/build.sbt
Expand Up @@ -10,6 +10,7 @@ libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "1.7.2" % "test"
)


seq(cucumberSettings : _*)

cucumberStepsBasePackage := "test"
Expand Down

0 comments on commit 0ec12b2

Please sign in to comment.