Skip to content

Commit 0ec12b2

Browse files
committed
Further work on allowiung cucumber to work as either a standalone 'cucumber' 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
1 parent 0fbc9a0 commit 0ec12b2

File tree

13 files changed

+135
-24
lines changed

13 files changed

+135
-24
lines changed

integration/src/main/scala/templemore/sbt/cucumber/CucumberFramework.scala

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package templemore.sbt.cucumber
22

33
import org.scalatools.testing._
44

5+
import java.util.Properties
6+
57
/**
68
* Framework implementation that allows cucumber to be run as part of sbt's
79
* standard 'test' phase.
@@ -13,21 +15,48 @@ class CucumberFramework extends Framework {
1315
val tests = Array[Fingerprint](CucumberRunOnceFingerprint)
1416

1517
def testRunner(testClassLoader: ClassLoader, loggers: Array[Logger]) = {
16-
//TODO
17-
println("%%% Creating a new runner...")
18-
new CucumberRunner()
18+
loggers foreach (_.debug("Creating a new Cucumber test runner"))
19+
new CucumberRunner(testClassLoader, loggers)
1920
}
2021
}
2122

22-
class CucumberRunner extends Runner2 {
23-
def run(testClassName: String, fingerprint: Fingerprint, eventHandler: EventHandler, args: Array[String]) = {
24-
println("%%% CUCUMBER RUNNER")
25-
println("%%% Running test class: " + testClassName)
26-
println("%%% with fingerprint: " + fingerprint)
27-
println("%%% with eventHandler: " + eventHandler)
28-
println("%%% with args: " + args.mkString(", "))
29-
//TODO
30-
}
23+
class CucumberRunner(testClassLoader: ClassLoader, loggers: Array[Logger]) extends Runner2 {
24+
private val cucumber = new ReflectingCucumberLauncher(debug = logDebug, error = logError)
25+
26+
def run(testClassName: String, fingerprint: Fingerprint, eventHandler: EventHandler, args: Array[String]) = try {
27+
val arguments = Array("--glue", "", "--format", "pretty", "classpath:")
28+
29+
cucumber(arguments, testClassLoader) match {
30+
case 0 =>
31+
logDebug("Cucumber tests completed successfully")
32+
eventHandler.handle(SuccessEvent(testClassName))
33+
case _ =>
34+
logDebug("Failure while running Cucumber tests")
35+
eventHandler.handle(FailureEvent(testClassName))
36+
}
37+
} catch {
38+
case e => eventHandler.handle(ErrorEvent(testClassName, e))
39+
}
40+
41+
private def logError(message: String) = loggers foreach (_ error message)
42+
private def logDebug(message: String) = loggers foreach (_ debug message)
43+
44+
case class SuccessEvent(testName: String) extends Event {
45+
val description = "Cucumber tests completed successfully."
46+
val result = Result.Success
47+
val error: Throwable = null
48+
}
49+
50+
case class FailureEvent(testName: String) extends Event {
51+
val description = "There were test failures (or undefined/pending steps)."
52+
val result = Result.Failure
53+
val error: Throwable = null
54+
}
55+
56+
case class ErrorEvent(testName: String, error: Throwable) extends Event {
57+
val description = "An error occurred while running Cucumber."
58+
val result = Result.Error
59+
}
3160
}
3261

3362
object CucumberRunOnceFingerprint extends SubclassFingerprint {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package templemore.sbt.cucumber
2+
3+
import java.lang.reflect.InvocationTargetException
4+
import java.util.Properties
5+
6+
class ReflectingCucumberLauncher(debug: (String) => Unit, error: (String) => Unit) {
7+
8+
private val RuntimeOptionsClassName = "cucumber.runtime.RuntimeOptions"
9+
private val MultiLoaderClassName = "cucumber.runtime.io.MultiLoader"
10+
private val MultiLoaderClassName_1_0_9 = "cucumber.io.MultiLoader"
11+
private val RuntimeClassName = "cucumber.runtime.Runtime"
12+
13+
def apply(cucumberArguments: Array[String],
14+
testClassLoader: ClassLoader): Int = {
15+
debug("Cucumber arguments: " + cucumberArguments.mkString(" "))
16+
val runtime = buildRuntime(System.getProperties, cucumberArguments, testClassLoader)
17+
runCucumber(runtime).asInstanceOf[Byte].intValue
18+
}
19+
20+
private def runCucumber(runtime: AnyRef) = try {
21+
val runtimeClass = runtime.getClass
22+
runtimeClass.getMethod("writeStepdefsJson").invoke(runtime)
23+
runtimeClass.getMethod("run").invoke(runtime)
24+
runtimeClass.getMethod("exitStatus").invoke(runtime)
25+
} catch {
26+
case e: InvocationTargetException => {
27+
val cause = if ( e.getCause == null ) e else e.getCause
28+
error("Error running cucumber. Cause: " + cause.getMessage)
29+
throw cause
30+
}
31+
}
32+
33+
private def buildRuntime(properties: Properties,
34+
arguments: Array[String],
35+
classLoader: ClassLoader): AnyRef = {
36+
def buildLoader(clazz: Class[_]) =
37+
clazz.getConstructor(classOf[ClassLoader]).newInstance(classLoader).asInstanceOf[AnyRef]
38+
def buildOptions(clazz: Class[_]) =
39+
clazz.getConstructor(classOf[Properties], classOf[Array[String]]).newInstance(properties.asInstanceOf[AnyRef], arguments).asInstanceOf[AnyRef]
40+
41+
val (runtimeClass, optionsClass, loaderClass) = loadCucumberClasses(classLoader)
42+
val runtimeConstructor = runtimeClass.getConstructor(loaderClass.getInterfaces()(0), classOf[ClassLoader], optionsClass)
43+
runtimeConstructor.newInstance(buildLoader(loaderClass), classLoader, buildOptions(optionsClass)).asInstanceOf[AnyRef]
44+
}
45+
46+
private def loadCucumberClasses(classLoader: ClassLoader) = try {
47+
val multiLoaderClassName = cucumberVersion(classLoader) match {
48+
case "1.0.9" => MultiLoaderClassName_1_0_9
49+
case _ => MultiLoaderClassName
50+
}
51+
52+
val runtimeOptionsClass = classLoader.loadClass(RuntimeOptionsClassName)
53+
val multiLoaderClass = classLoader.loadClass(multiLoaderClassName)
54+
val runtimeClass = classLoader.loadClass(RuntimeClassName)
55+
(runtimeClass, runtimeOptionsClass, multiLoaderClass)
56+
} catch {
57+
case e: ClassNotFoundException =>
58+
error("Unable to load Cucumber classes. Please check your project dependencied. (Details: " + e.getMessage + ")")
59+
throw e
60+
}
61+
62+
private def cucumberVersion(classLoader: ClassLoader) = {
63+
val stream = classLoader.getResourceAsStream("cucumber/version.properties")
64+
try {
65+
val props = new Properties()
66+
props.load(stream)
67+
val version = props.getProperty("cucumber-jvm.version")
68+
debug("Determined cucumber-jvm version to be: " + version)
69+
version
70+
} finally {
71+
stream.close()
72+
}
73+
}
74+
}

plugin/src/main/scala/templemore/sbt/cucumber/CucumberPlugin.scala

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ object CucumberPlugin extends Plugin with Integration {
2727
val cucumberJVMOptions = SettingKey[List[String]]("cucumber-jvm-options")
2828

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

@@ -53,8 +53,8 @@ object CucumberPlugin extends Plugin with Integration {
5353
}
5454

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

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

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

plugin/src/main/scala/templemore/sbt/cucumber/Integration.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ trait Integration {
6262
output.options ++
6363
makeOptionsList(tags, "--tags") ++
6464
makeOptionsList(names, "--name") ++
65-
(options.featuresDir.getPath :: Nil)
65+
(options.featuresLocation :: Nil)
6666
JvmLauncher(jvmSettings).launch(cucumberParams)
6767
}
6868
}

plugin/src/main/scala/templemore/sbt/cucumber/Options.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import java.io.File
77
*
88
* @author Chris Turner
99
*/
10-
case class Options(featuresDir: File,
10+
case class Options(featuresLocation: String,
1111
basePackage: String,
1212
extraOptions: List[String],
1313
beforeFunc: () => Unit,
1414
afterFunc: () => Unit) {
1515

16-
def featuresPresent = featuresDir.exists
16+
def featuresPresent = featuresLocation.startsWith("classpath:") || (new File(featuresLocation).exists)
1717
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
target
3+
project/boot
4+
project/target
5+
project/plugins/target
6+
project/plugins/project

testProjects/testInterfaceProject/build.sbt renamed to testProjects/testIntegrationProject/build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name := "test-interface-project"
1+
name := "test-project"
22

33
version := "0.7.0"
44

@@ -7,7 +7,7 @@ organization := "templemore"
77
scalaVersion := "2.9.2"
88

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

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
resolvers += "Templemore Repository" at "http://templemore.co.uk/repo"
22

33
addSbtPlugin("templemore" % "sbt-cucumber-plugin" % "0.7.0")
4+
5+

0 commit comments

Comments
 (0)