diff --git a/README.md b/README.md index 74fdc992..c4247cdb 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,44 @@ To use cucumber-jvm-scala in your project, add the following dependency to your test ``` + +## Compatibility matrix + +The Cucumber Scala version matches the Cucumber version except for the bugfix number +which can be different. + +| Cucumber Scala version | Cucumber version | Scala versions | +|------------------------|------------------|------------------| +| 5.6.0 | 5.6.0 | 2.11, 2.12, 2.13 | +| 4.7.1 | 4.7.1 | 2.11, 2.12, 2.13 | + +## Migrating from 4.x to 5.x + +If you are using Cucumber Scala 4.7.x and want to upgrade to 5.x, please note there are some major changes in addition to the Cucumber upgrade itself. + +### Packages + +All Cucumber Scala classes are now under `io.cucumber.scala` package instead of `cucumber.api.scala`. + +### Before/BeforeStep/After/AfterStep definitions + +The `Before`, `BeforeStep`, `After` and `AfterStep` definitions have slightly changed: +- to apply only with some tags, the variable list of tags as `String*` is replaced by a single tag expression of type `String`. +- if providing both an order and a tag expression, the order is now the second parameter instead of the first. +This is more consistent with the Java implementation. + +For instance, the following code: + +```scala +Before(1, "@tag1", "@tag2") { _ => + // Do Something +} +``` + +Is replaced by: + +```scala +Before("@tag1 or @tag2", 1) { _ => + // Do Something +} +``` \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index 2285ef93..3eeb5679 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm-scala - 4.7.2-SNAPSHOT + 5.6.0-SNAPSHOT scala-examples diff --git a/examples/src/test/scala/cucumber/examples/scalacalculator/RpnCalculatorStepDefinitions.scala b/examples/src/test/scala/cucumber/examples/scalacalculator/RpnCalculatorStepDefinitions.scala index a86e7ea3..e6f253b2 100644 --- a/examples/src/test/scala/cucumber/examples/scalacalculator/RpnCalculatorStepDefinitions.scala +++ b/examples/src/test/scala/cucumber/examples/scalacalculator/RpnCalculatorStepDefinitions.scala @@ -1,7 +1,6 @@ package cucumber.examples.scalacalculator -import cucumber.api.Scenario -import cucumber.api.scala.{ScalaDsl, EN} +import io.cucumber.scala.{EN, ScalaDsl, Scenario} import org.junit.Assert._ class RpnCalculatorStepDefinitions extends ScalaDsl with EN { diff --git a/examples/src/test/scala/cucumber/examples/scalacalculator/RunCukesTest.scala b/examples/src/test/scala/cucumber/examples/scalacalculator/RunCukesTest.scala index 37f2ba23..3b2684a2 100644 --- a/examples/src/test/scala/cucumber/examples/scalacalculator/RunCukesTest.scala +++ b/examples/src/test/scala/cucumber/examples/scalacalculator/RunCukesTest.scala @@ -1,8 +1,8 @@ package cucumber.examples.scalacalculator -import cucumber.api.CucumberOptions +import io.cucumber.junit.{Cucumber, CucumberOptions} import org.junit.runner.RunWith -import cucumber.api.junit.Cucumber @RunWith(classOf[Cucumber]) +@CucumberOptions() class RunCukesTest \ No newline at end of file diff --git a/pom.xml b/pom.xml index cb096fc0..d9289c3e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,207 +1,208 @@ - - 4.0.0 - io.cucumber - cucumber-jvm-scala - 4.7.2-SNAPSHOT - pom - Cucumber-JVM: Scala - Cucumber for Scala - http://cucumber.io/ - - - io.cucumber - cucumber-parent - 1.0.2 - - - - - glib-briia - Glib Briia - glib.briia@gmail.com - AssertThat - https://www.assertthat.com - - - - - UTF-8 - UTF-8 - 3.3 - ${project.build.directory} - 3.2.2 - 4.7.1 - 5.1.0 - 2.4.12 - 4.12 - 1.10.19 - 2.12.7 - 2.11.12 - 2.13.0 - - - - scm:git:git://github.com/cucumber/cucumber-jvm-scala.git - scm:git:git@github.com:cucumber/cucumber-jvm-scala.git - git://github.com/cucumber/cucumber-jvm-scala.git - v4.3.0 - - - - - - io.cucumber - cucumber-core - ${cucumber.version} - - - io.cucumber - cucumber-junit - ${cucumber.version} - test - - - junit - junit - ${junit.version} - test - - - org.mockito - mockito-core - ${mockito.version} - test - - - - - - scala - - - - - examples - - true - - - examples - - - - - coveralls.io - - - - - org.codehaus.mojo - cobertura-maven-plugin - ${cobertura-maven-plugin.version} - - - xml - - - true - - true - - - - - - org.eluder.coveralls - coveralls-maven-plugin - 4.3.0 - - - ${basedir}/scala/scala_2.11/target/generated-sources/i18n - ${basedir}/scala/scala_2.12/target/generated-sources/i18n - ${basedir}/scala/scala_2.13/target/generated-sources/i18n - - - - - - - - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.8 - - - org.codehaus.groovy - groovy-all - ${groovy.version} - - - io.cucumber - gherkin - ${gherkin.version} - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.0.0-M1 - - false - cucumber.runtime,cucumber.runtime.* - - http://docs.oracle.com/javase/7/docs/api/ - http://junit.sourceforge.net/javadoc/ - - - - Main API Packages - cucumber.api:cucumber.api.* - - - I18n - Java - cucumber.api.java.* - - - - - java - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.0.0 - - - package - - shade - - - true - - - - - - - - net.alchim31.maven - scala-maven-plugin - ${scala-maven-plugin.version} - - - - - - + + 4.0.0 + io.cucumber + cucumber-jvm-scala + 5.6.0-SNAPSHOT + pom + Cucumber-JVM: Scala + Cucumber for Scala + http://cucumber.io/ + + + io.cucumber + cucumber-parent + 1.0.2 + + + + + glib-briia + Glib Briia + glib.briia@gmail.com + AssertThat + https://www.assertthat.com + + + + + UTF-8 + UTF-8 + 3.3 + ${project.build.directory} + 3.4.6 + 5.6.0 + 5.1.0 + 2.4.19 + 2.10.3 + 4.12 + 2.11.12 + 2.12.11 + 2.13.1 + 1.13.9 + + + + scm:git:git://github.com/cucumber/cucumber-jvm-scala.git + scm:git:git@github.com:cucumber/cucumber-jvm-scala.git + git://github.com/cucumber/cucumber-jvm-scala.git + v4.3.0 + + + + + + io.cucumber + cucumber-core + ${cucumber.version} + + + io.cucumber + cucumber-junit + ${cucumber.version} + test + + + junit + junit + ${junit.version} + test + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-databind.version} + test + + + + + + scala + + + + + examples + + true + + + examples + + + + + coveralls.io + + + + + org.codehaus.mojo + cobertura-maven-plugin + ${cobertura-maven-plugin.version} + + + xml + + + true + + true + + + + + + org.eluder.coveralls + coveralls-maven-plugin + 4.3.0 + + + ${basedir}/scala/scala_2.11/target/generated-sources/i18n + ${basedir}/scala/scala_2.12/target/generated-sources/i18n + ${basedir}/scala/scala_2.13/target/generated-sources/i18n + + + + + + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + org.codehaus.groovy + groovy-all + ${groovy.version} + + + io.cucumber + gherkin + ${gherkin.version} + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.0-M1 + + false + cucumber.runtime,cucumber.runtime.* + + http://docs.oracle.com/javase/7/docs/api/ + http://junit.sourceforge.net/javadoc/ + + + + Main API Packages + cucumber.api:cucumber.api.* + + + I18n - Java + cucumber.api.java.* + + + + + java + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.0.0 + + + package + + shade + + + true + + + + + + + + net.alchim31.maven + scala-maven-plugin + ${scala-maven-plugin.version} + + + + + + diff --git a/scala/pom.xml b/scala/pom.xml index 8b0f00ad..d05f1c69 100644 --- a/scala/pom.xml +++ b/scala/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-jvm-scala - 4.7.2-SNAPSHOT + 5.6.0-SNAPSHOT cucumber-scala-aggregator @@ -43,6 +43,11 @@ junit test + + com.fasterxml.jackson.core + jackson-databind + test + @@ -112,7 +117,7 @@ def unsupported = ["em"] def dialectProvider = new GherkinDialectProvider() def binding = ["dialectProvider":dialectProvider, "unsupported":unsupported] template = engine.createTemplate(templateSource).make(binding) -def file = new File(project.baseDir, "target${File.separator}generated-sources${File.separator}i18n${File.separator}cucumber${File.separator}api${File.separator}scala${File.separator}I18n.scala") +def file = new File(project.baseDir, "target${File.separator}generated-sources${File.separator}i18n${File.separator}io${File.separator}cucumber${File.separator}scala${File.separator}I18n.scala") file.parentFile.mkdirs() file.write(template.toString(), "UTF-8") ]]> diff --git a/scala/scala_2.11/pom.xml b/scala/scala_2.11/pom.xml index 5b2325cc..06304078 100644 --- a/scala/scala_2.11/pom.xml +++ b/scala/scala_2.11/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-scala-aggregator - 4.7.2-SNAPSHOT + 5.6.0-SNAPSHOT cucumber-scala_2.11 @@ -25,10 +25,22 @@ ${scala.2.11.version} test + + + org.mockito + mockito-scala_2.11 + ${mockito-scala.version} + test + ../sources/src/main/scala + + + ../sources/src/main/resources + + ../sources/src/test/scala diff --git a/scala/scala_2.12/pom.xml b/scala/scala_2.12/pom.xml index 1bfc3757..41651e9f 100644 --- a/scala/scala_2.12/pom.xml +++ b/scala/scala_2.12/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-scala-aggregator - 4.7.2-SNAPSHOT + 5.6.0-SNAPSHOT cucumber-scala_2.12 @@ -25,10 +25,23 @@ ${scala.2.12.version} test + + + org.mockito + mockito-scala_2.12 + ${mockito-scala.version} + test + + ../sources/src/main/scala + + + ../sources/src/main/resources + + ../sources/src/test/scala diff --git a/scala/scala_2.13/pom.xml b/scala/scala_2.13/pom.xml index baf9c24c..59c345ff 100644 --- a/scala/scala_2.13/pom.xml +++ b/scala/scala_2.13/pom.xml @@ -4,7 +4,7 @@ io.cucumber cucumber-scala-aggregator - 4.7.2-SNAPSHOT + 5.6.0-SNAPSHOT cucumber-scala_2.13 @@ -25,10 +25,23 @@ ${scala.2.13.version} test + + + org.mockito + mockito-scala_2.13 + ${mockito-scala.version} + test + + ../sources/src/main/scala + + + ../sources/src/main/resources + + ../sources/src/test/scala diff --git a/scala/sources/src/main/code_generator/I18n.scala.txt b/scala/sources/src/main/code_generator/I18n.scala.txt index ba02449b..1579c38a 100644 --- a/scala/sources/src/main/code_generator/I18n.scala.txt +++ b/scala/sources/src/main/code_generator/I18n.scala.txt @@ -1,4 +1,4 @@ -package cucumber.api.scala +package io.cucumber.scala <% gherkin.GherkinDialectProvider.DIALECTS.keySet().findAll { !unsupported.contains(it) }.each { language -> %> trait ${language.replaceAll("[\\s-]", "_").toUpperCase()} { diff --git a/scala/sources/src/main/resources/META-INF/services/io.cucumber.core.backend.BackendProviderService b/scala/sources/src/main/resources/META-INF/services/io.cucumber.core.backend.BackendProviderService new file mode 100644 index 00000000..85d09655 --- /dev/null +++ b/scala/sources/src/main/resources/META-INF/services/io.cucumber.core.backend.BackendProviderService @@ -0,0 +1 @@ +io.cucumber.scala.ScalaBackendProviderService \ No newline at end of file diff --git a/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaBackend.scala b/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaBackend.scala deleted file mode 100644 index 38629e38..00000000 --- a/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaBackend.scala +++ /dev/null @@ -1,93 +0,0 @@ -package cucumber.runtime.scala - -import java.lang.reflect.Modifier -import java.net.URI -import java.util.{List => JList} - -import cucumber.api.scala.ScalaDsl -import cucumber.runtime.{Backend, Glue} -import cucumber.runtime.io.{ResourceLoader, ResourceLoaderClassFinder} -import cucumber.runtime.snippets.{FunctionNameGenerator, SnippetGenerator} -import gherkin.pickles.PickleStep -import io.cucumber.stepexpression.TypeRegistry - -import scala.collection.JavaConverters._ - -class ScalaBackend(resourceLoader: ResourceLoader, typeRegistry: TypeRegistry) extends Backend { - private val snippetGenerator = new SnippetGenerator(new ScalaSnippetGenerator(), typeRegistry.parameterTypeRegistry()) - private var instances: Seq[ScalaDsl] = Nil - - - def getStepDefinitions = instances.flatMap(_.getStepDefs(typeRegistry)) - - def getBeforeHooks = instances.flatMap(_.beforeHooks) - - def getAfterHooks = instances.flatMap(_.afterHooks) - - def getAfterStepHooks = instances.flatMap(_.afterStepHooks) - - def getBeforeStepHooks = instances.flatMap(_.beforeStepHooks) - - def disposeWorld(): Unit = { - instances = Nil - } - - def getSnippet(step: PickleStep, keyword: String, functionNameGenerator: FunctionNameGenerator) = snippetGenerator.getSnippet(step, keyword, functionNameGenerator) - - def buildWorld(): Unit = { - //I don't believe scala has to do anything to clean out its world - } - - def loadGlue(glue: Glue, gluePaths: JList[URI]): Unit = { - - val cl = Thread.currentThread().getContextClassLoader - val classFinder = new ResourceLoaderClassFinder(resourceLoader, cl) - val packages = gluePaths.asScala - val dslClasses = packages flatMap { - classFinder.getDescendants(classOf[ScalaDsl], _).asScala - } filter { cls => - try { - cls.getDeclaredConstructor() - true - } catch { - case e: Throwable => false - } - } - - val (clsClasses, objClasses) = dslClasses partition { cls => - try { - Modifier.isPublic(cls.getConstructor().getModifiers) - } catch { - case e: Throwable => false - } - } - val objInstances = objClasses map { cls => - val instField = cls.getDeclaredField("MODULE$") - instField.setAccessible(true) - instField.get(null).asInstanceOf[ScalaDsl] - } - val clsInstances = clsClasses.map { - _.newInstance() - } - - instances = objInstances.toSeq ++ clsInstances - - getStepDefinitions map { - glue.addStepDefinition(_) - } - getBeforeHooks map { - glue.addBeforeHook(_) - } - getAfterHooks map { - glue.addAfterHook(_) - } - getAfterStepHooks map { - glue.addAfterStepHook(_) - } - getBeforeStepHooks map { - glue.addBeforeStepHook(_) - } - () - } - -} diff --git a/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaHookDefinition.scala b/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaHookDefinition.scala deleted file mode 100644 index 89a4bb16..00000000 --- a/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaHookDefinition.scala +++ /dev/null @@ -1,27 +0,0 @@ -package cucumber.runtime.scala - -import gherkin.pickles.PickleTag -import java.util.Collection - -import cucumber.api.Scenario -import cucumber.runtime.HookDefinition -import cucumber.runtime.filter.TagPredicate - -import scala.collection.JavaConverters._ - -class ScalaHookDefinition(f:Scenario => Unit, - order:Int, - tags:Seq[String]) extends HookDefinition { - - val tagPredicate = new TagPredicate(tags.asJava) - - def getLocation(detail: Boolean) = "TODO: Implement getLocation in similar fashion to ScalaStepDefinition" - - def execute(scenario: Scenario): Unit = f(scenario) - - def matches(tags: Collection[PickleTag]) = tagPredicate.apply(tags) - - def getOrder = order - - def isScenarioScoped = false -} diff --git a/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaSnippetGenerator.scala b/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaSnippetGenerator.scala deleted file mode 100644 index 92a8c3c8..00000000 --- a/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaSnippetGenerator.scala +++ /dev/null @@ -1,42 +0,0 @@ -package cucumber.runtime.scala - -import java.lang.reflect.Type -import java.util - -import cucumber.runtime.snippets.Snippet - -import collection.JavaConverters._ - -class ScalaSnippetGenerator extends Snippet { - - def template() = - "{0}(\"\"\"{1}\"\"\")'{' ({3}) =>\n" + - " //// {4}\n" + - " throw new PendingException()\n" + - "'}'" - - def tableHint() = null - - def namedGroupStart() = null - - def namedGroupEnd() = null - - def escapePattern(pattern:String) = pattern - - override def arguments(map: util.Map[String, Type]): String = { - val indexed = map.asScala.zipWithIndex - - def name(clazz: Class[_]) = - if(clazz.isPrimitive){ - val name = clazz.getName - s"${name.charAt(0).toUpper}${name.substring(1)}" - } else - clazz.getSimpleName - - val named = indexed.map { - case (c, i) => "arg" + i + ":" + name(c._2.getClass) - } - - named.mkString(", ") - } -} diff --git a/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaStepDefinition.scala b/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaStepDefinition.scala deleted file mode 100644 index c7bfe0c1..00000000 --- a/scala/sources/src/main/scala/cucumber/runtime/scala/ScalaStepDefinition.scala +++ /dev/null @@ -1,92 +0,0 @@ -package cucumber.runtime.scala - -import java.lang.reflect.Type -import cucumber.runtime.StepDefinition -import gherkin.pickles.PickleStep -import io.cucumber.stepexpression._ - -/** - * Implementation of step definition for scala. - * - * @param frame Representation of a stack frame containing information about the context in which a - * step was defined. Allows retrospective queries about the definition of a step. - * - * @param name The name of the step definition class, e.g. cucumber.runtime.scala.test.CukesStepDefinitions - * - * @param pattern The regex matcher that defines the cucumber step, e.g. /I eat (.*) cukes$/ - - * @param parameterInfos - * - * @param f Function body of a step definition. This is what actually runs the code within the step def. - */ -class ScalaStepDefinition(frame:StackTraceElement, - name:String, - pattern:String, - parameterInfos:Array[Type], - f:List[Any] => Any) extends StepDefinition { - - private[cucumber] var typeRegistry: TypeRegistry = null - private var expression: StepExpression = null - - /** - * Returns a list of arguments. Return null if the step definition - * doesn't match at all. Return an empty List if it matches with 0 arguments - * and bigger sizes if it matches several. - */ - def matchedArguments(step: PickleStep) = { - expression = createExpression(pattern, typeRegistry) - val argumentMatcher = new ExpressionArgumentMatcher(expression) - argumentMatcher.argumentsFrom(step, parameterInfos:_*) - } - - private def createExpression(expression: String, typeRegistry: TypeRegistry): StepExpression = { - if (parameterInfos.isEmpty) new StepExpressionFactory(typeRegistry).createExpression(expression) - else { - val parameterInfo = parameterInfos(parameterInfos.size - 1) - new StepExpressionFactory(typeRegistry).createExpression(expression, parameterInfo, false) - } - } - - /** - * The source line where the step definition is defined. - * Example: foo/bar/Zap.brainfuck:42 - * - * @param detail true if extra detailed location information should be included. - */ - override def getLocation(detail: Boolean) = frame.getFileName + ":" + frame.getLineNumber - - /** - * How many declared parameters this stepdefinition has. Returns null if unknown. - */ - override def getParameterCount() = parameterInfos.size - - /** - * The parameter type at index n. A hint about the raw parameter type is passed to make - * it easier for the implementation to make a guess based on runtime information. - * As Scala is a statically typed language, the javaType parameter is ignored - */ - def getParameterType(index: Int, javaType: Type) = { - parameterInfos(index) - } - - /** - * Invokes the step definition. The method should raise a Throwable - * if the invocation fails, which will cause the step to fail. - */ - override def execute(args: Array[AnyRef]): Unit = { - f(args.toList) - } - - /** - * Return true if this matches the location. This is used to filter - * stack traces. - */ - override def isDefinedAt(stackTraceElement: StackTraceElement) = stackTraceElement == frame - - /** - * @return the pattern associated with this instance. Used for error reporting only. - */ - override def getPattern = pattern - - override def isScenarioScoped = false -} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/AbstractGlueDefinition.scala b/scala/sources/src/main/scala/io/cucumber/scala/AbstractGlueDefinition.scala new file mode 100644 index 00000000..9813d39e --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/AbstractGlueDefinition.scala @@ -0,0 +1,15 @@ +package io.cucumber.scala + +import io.cucumber.core.backend.Located + +abstract class AbstractGlueDefinition(location: StackTraceElement) extends Located { + + def getLocation(): String = { + location.toString + } + + def isDefinedAt(stackTraceElement: StackTraceElement): Boolean = { + location.getFileName != null && location.getFileName == stackTraceElement.getFileName + } + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/Aliases.scala b/scala/sources/src/main/scala/io/cucumber/scala/Aliases.scala new file mode 100644 index 00000000..ff588ef3 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/Aliases.scala @@ -0,0 +1,12 @@ +package io.cucumber.scala + +/** + * Contains some aliases to help match this codebase with cucumber-java + */ +object Aliases { + + type HookBody = Scenario => Unit + + type StepDefinitionBody = () => Unit + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/GlueAdaptor.scala b/scala/sources/src/main/scala/io/cucumber/scala/GlueAdaptor.scala new file mode 100644 index 00000000..8d910885 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/GlueAdaptor.scala @@ -0,0 +1,15 @@ +package io.cucumber.scala + +import io.cucumber.core.backend.Glue + +class GlueAdaptor(glue: Glue) { + + def addDefinition(registry: ScalaDslRegistry): Unit = { + registry.stepDefinitions.foreach(glue.addStepDefinition) + registry.beforeHooks.foreach(glue.addBeforeHook) + registry.afterHooks.foreach(glue.addAfterHook) + registry.afterStepHooks.foreach(glue.addAfterStepHook) + registry.beforeStepHooks.foreach(glue.addBeforeStepHook) + } + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/PendingException.scala b/scala/sources/src/main/scala/io/cucumber/scala/PendingException.scala new file mode 100644 index 00000000..021884aa --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/PendingException.scala @@ -0,0 +1,5 @@ +package io.cucumber.scala + +class PendingException extends RuntimeException("TODO: implement me") { + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/ScalaBackend.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaBackend.scala new file mode 100644 index 00000000..ae943dae --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaBackend.scala @@ -0,0 +1,70 @@ +package io.cucumber.scala + +import java.lang.reflect.Modifier +import java.net.URI +import java.util.function.Supplier +import java.util.{List => JList} + +import io.cucumber.core.backend._ +import io.cucumber.core.resource.{ClasspathScanner, ClasspathSupport} + +import scala.collection.JavaConverters._ +import scala.util.Try + +class ScalaBackend(classLoaderProvider: Supplier[ClassLoader]) extends Backend { + + private val classFinder = new ClasspathScanner(classLoaderProvider) + + private[scala] var scalaGlueInstances: Seq[ScalaDsl] = Nil + + override def disposeWorld(): Unit = { + scalaGlueInstances = Nil + } + + override def getSnippet(): Snippet = { + new ScalaSnippet() + } + + override def buildWorld(): Unit = { + // Nothing to do + } + + override def loadGlue(glue: Glue, gluePaths: JList[URI]): Unit = { + + val dslClasses = gluePaths.asScala + .filter(gluePath => ClasspathSupport.CLASSPATH_SCHEME.equals(gluePath.getScheme)) + .map(ClasspathSupport.packageName) + .flatMap(basePackageName => classFinder.scanForSubClassesInPackage(basePackageName, classOf[ScalaDsl]).asScala) + .filter(glueClass => !glueClass.isInterface) + .filter(glueClass => glueClass.getConstructors.length > 0) + + val (clsClasses, objClasses) = dslClasses.partition(isRegularClass) + + val objInstances = objClasses.map { cls => + val instField = cls.getDeclaredField("MODULE$") + instField.setAccessible(true) + instField.get(null).asInstanceOf[ScalaDsl] + } + val clsInstances = clsClasses.map { + _.newInstance() + } + + // FIXME we should not create instances above but fill the container like Cucumber Java does + // https://github.com/cucumber/cucumber-jvm-scala/issues/1 + //clsClasses.foreach(container.addClass(_)) + scalaGlueInstances = objInstances.toSeq ++ clsInstances + + val glueAdaptor = new GlueAdaptor(glue) + + scalaGlueInstances.foreach { glueInstance => + glueAdaptor.addDefinition(glueInstance.registry) + } + + () + } + + private def isRegularClass(cls: Class[_]): Boolean = { + Try(Modifier.isPublic(cls.getConstructor().getModifiers)).getOrElse(false) + } + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/ScalaBackendProviderService.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaBackendProviderService.scala new file mode 100644 index 00000000..0a3fdec3 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaBackendProviderService.scala @@ -0,0 +1,13 @@ +package io.cucumber.scala + +import java.util.function.Supplier + +import io.cucumber.core.backend.{Backend, BackendProviderService, Container, Lookup} + +class ScalaBackendProviderService extends BackendProviderService { + + override def create(lookup: Lookup, container: Container, classLoaderSupplier: Supplier[ClassLoader]): Backend = { + new ScalaBackend(classLoaderSupplier) + } + +} diff --git a/scala/sources/src/main/scala/cucumber/api/scala/ScalaDsl.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaDsl.scala similarity index 89% rename from scala/sources/src/main/scala/cucumber/api/scala/ScalaDsl.scala rename to scala/sources/src/main/scala/io/cucumber/scala/ScalaDsl.scala index 0290a743..b6ba62c3 100644 --- a/scala/sources/src/main/scala/cucumber/api/scala/ScalaDsl.scala +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaDsl.scala @@ -1,83 +1,90 @@ -package cucumber.api.scala +package io.cucumber.scala import java.lang.reflect.{ParameterizedType, Type} -import cucumber.api.Scenario -import cucumber.runtime.{HookDefinition, StepDefinition} -import cucumber.runtime.scala.{ScalaHookDefinition, ScalaStepDefinition} -import io.cucumber.stepexpression.TypeRegistry +import io.cucumber.core.backend.{HookDefinition, StepDefinition} +import io.cucumber.scala.Aliases.HookBody import scala.collection.mutable.ArrayBuffer - /** - * Base trait for a scala step definition implementation. - */ + * Base trait for a scala step definition implementation. + */ trait ScalaDsl { self => + + val EMPTY_TAG_EXPRESSION = "" + val DEFAULT_BEFORE_ORDER = 1000 + val DEFAULT_AFTER_ORDER = 1000 + import scala.language.implicitConversions - private val stepDefinitions = new ArrayBuffer[StepDefinition] + val registry = new ScalaDslRegistry() - private[cucumber] val beforeHooks = new ArrayBuffer[HookDefinition] + // TODO support Before/After with no parameter - private[cucumber] val beforeStepHooks = new ArrayBuffer[HookDefinition] + def Before(body: HookBody): Unit = { + Before(EMPTY_TAG_EXPRESSION, DEFAULT_BEFORE_ORDER)(body) + } - private[cucumber] val afterHooks = new ArrayBuffer[HookDefinition] + def Before(tagExpression: String)(body: HookBody): Unit = { + Before(tagExpression, DEFAULT_BEFORE_ORDER)(body) + } - private[cucumber] val afterStepHooks = new ArrayBuffer[HookDefinition] + def Before(order: Int)(body: HookBody): Unit = { + Before(EMPTY_TAG_EXPRESSION, order)(body) + } - private[cucumber] def getStepDefs(typeRegistry: TypeRegistry): ArrayBuffer[StepDefinition]={ - stepDefinitions.foreach(d=>d.asInstanceOf[ScalaStepDefinition].typeRegistry = typeRegistry) - stepDefinitions + def Before(tagExpression: String, order: Int)(body: HookBody): Unit = { + registry.beforeHooks += new ScalaHookDefinition(tagExpression, order, body) } - def Before(f: Scenario => Unit) : Unit = { - Before()(f) + def BeforeStep(body: HookBody): Unit = { + BeforeStep(EMPTY_TAG_EXPRESSION, DEFAULT_BEFORE_ORDER)(body) } - def Before(tags: String*)(f: Scenario => Unit) : Unit = { - Before(Int.MaxValue, tags: _*)(f) + def BeforeStep(tagExpression: String)(body: HookBody): Unit = { + BeforeStep(tagExpression, DEFAULT_BEFORE_ORDER)(body) } - def Before(order: Int, tags: String*)(f: Scenario => Unit) : Unit = { - beforeHooks += new ScalaHookDefinition(f, order, tags) + def BeforeStep(order: Int)(body: HookBody): Unit = { + BeforeStep(EMPTY_TAG_EXPRESSION, order)(body) } - def BeforeStep(f: Scenario => Unit) : Unit = { - BeforeStep()(f) + def BeforeStep(tagExpression: String, order: Int)(body: HookBody): Unit = { + registry.beforeStepHooks += new ScalaHookDefinition(tagExpression, order, body) } - def BeforeStep(tags: String*)(f: Scenario => Unit) : Unit = { - BeforeStep(Int.MaxValue, tags: _*)(f) + def After(body: HookBody): Unit = { + After(EMPTY_TAG_EXPRESSION, DEFAULT_AFTER_ORDER)(body) } - def BeforeStep(order: Int, tags: String*)(f: Scenario => Unit) : Unit = { - beforeStepHooks += new ScalaHookDefinition(f, order, tags) + def After(tagExpression: String)(body: HookBody): Unit = { + After(tagExpression, DEFAULT_AFTER_ORDER)(body) } - def After(f: Scenario => Unit) : Unit = { - After()(f) + def After(order: Int)(body: HookBody): Unit = { + After(EMPTY_TAG_EXPRESSION, order)(body) } - def After(tags: String*)(f: Scenario => Unit) : Unit = { - After(Int.MaxValue, tags: _*)(f) + def After(tagExpression: String, order: Int)(body: HookBody): Unit = { + registry.afterHooks += new ScalaHookDefinition(tagExpression, order, body) } - def After(order: Int, tags: String*)(f: Scenario => Unit) : Unit = { - afterHooks += new ScalaHookDefinition(f, order, tags) + def AfterStep(body: HookBody): Unit = { + AfterStep(EMPTY_TAG_EXPRESSION, DEFAULT_AFTER_ORDER)(body) } - def AfterStep(f: Scenario => Unit) : Unit = { - AfterStep()(f) + def AfterStep(tagExpression: String)(body: HookBody): Unit = { + AfterStep(tagExpression, DEFAULT_AFTER_ORDER)(body) } - def AfterStep(tags: String*)(f: Scenario => Unit) : Unit = { - AfterStep(Int.MaxValue, tags: _*)(f) + def AfterStep(order: Int)(body: HookBody): Unit = { + AfterStep(EMPTY_TAG_EXPRESSION, order)(body) } - def AfterStep(order: Int, tags: String*)(f: Scenario => Unit) : Unit = { - afterStepHooks += new ScalaHookDefinition(f, order, tags) + def AfterStep(tagExpression: String, order: Int)(body: HookBody): Unit = { + registry.afterStepHooks += new ScalaHookDefinition(tagExpression, order, body) } @@ -88,7 +95,7 @@ trait ScalaDsl { final class Fun0(val f: Function0[Any]) object Fun0 { - implicit def function0AsFun0(f: Function0[Any]) = new Fun0(f) + implicit def function0AsFun0(f: Function0[Any]): Fun0 = new Fun0(f) } final class StepBody(name: String, regex: String) { @@ -115,11 +122,11 @@ trait ScalaDsl { * Fun0 and the implicit conversion lets us work around this. * **/ - def apply(f: => Unit) : Unit = { + def apply(f: => Unit): Unit = { apply(f _) } - def apply(fun: Fun0) : Unit = { + def apply(fun: Fun0): Unit = { register() { case Nil => fun.f() } @@ -338,8 +345,8 @@ trait ScalaDsl { } } - def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16](f: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16) => Any)(implicit m1: Manifest[T1], m2: Manifest[T2], m3: Manifest[T3], m4: Manifest[T4], m5: Manifest[T5], m6: Manifest[T6], m7: Manifest[T7], m8: Manifest[T8], m9: Manifest[T9], m10: Manifest[T10], m11: Manifest[T11], m12: Manifest[T12], m13: Manifest[T13], m14: Manifest[T14], m15: Manifest[T15], m16: Manifest[T16]) = { - register(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16) { + def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16](f: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16) => Any)(implicit m1: Manifest[T1], m2: Manifest[T2], m3: Manifest[T3], m4: Manifest[T4], m5: Manifest[T5], m6: Manifest[T6], m7: Manifest[T7], m8: Manifest[T8], m9: Manifest[T9], m10: Manifest[T10], m11: Manifest[T11], m12: Manifest[T12], m13: Manifest[T13], m14: Manifest[T14], m15: Manifest[T15], m16: Manifest[T16]) = { + register(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16) { case List(a1: AnyRef, a2: AnyRef, a3: AnyRef, a4: AnyRef, a5: AnyRef, a6: AnyRef, a7: AnyRef, a8: AnyRef, a9: AnyRef, a10: AnyRef, a11: AnyRef, a12: AnyRef, a13: AnyRef, a14: AnyRef, a15: AnyRef, a16: AnyRef) => f(a1.asInstanceOf[T1], a2.asInstanceOf[T2], @@ -486,7 +493,7 @@ trait ScalaDsl { } def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22](f: (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22) => Any)(implicit m1: Manifest[T1], m2: Manifest[T2], m3: Manifest[T3], m4: Manifest[T4], m5: Manifest[T5], m6: Manifest[T6], m7: Manifest[T7], m8: Manifest[T8], m9: Manifest[T9], m10: Manifest[T10], m11: Manifest[T11], m12: Manifest[T12], m13: Manifest[T13], m14: Manifest[T14], m15: Manifest[T15], m16: Manifest[T16], m17: Manifest[T17], m18: Manifest[T18], m19: Manifest[T19], m20: Manifest[T20], m21: Manifest[T21], m22: Manifest[T22]) = { - register(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20, m21, m22) { + register(m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20, m21, m22) { case List(a1: AnyRef, a2: AnyRef, a3: AnyRef, a4: AnyRef, a5: AnyRef, a6: AnyRef, a7: AnyRef, a8: AnyRef, a9: AnyRef, a10: AnyRef, a11: AnyRef, a12: AnyRef, a13: AnyRef, a14: AnyRef, a15: AnyRef, a16: AnyRef, a17: AnyRef, a18: AnyRef, a19: AnyRef, a20: AnyRef, a21: AnyRef, a22: AnyRef) => f(a1.asInstanceOf[T1], a2.asInstanceOf[T2], @@ -513,9 +520,9 @@ trait ScalaDsl { } } - private def register(ms: Manifest[_ <: Any]*)(pf: PartialFunction[List[Any], Any]) = { + private def register(ms: Manifest[_ <: Any]*)(pf: PartialFunction[List[Any], Any]): Unit = { val types = ms.map(m => toJavaType(m)).toArray - stepDefinitions += new ScalaStepDefinition(frame, name, regex, types, pf) + registry.stepDefinitions += new ScalaStepDefinition(Utils.frame(self), name, regex, types, pf) } private def toJavaType(m: Manifest[_]): Type = { @@ -524,7 +531,7 @@ trait ScalaDsl { m.runtimeClass } else { new ParameterizedType { - override def getActualTypeArguments: Array[Type] = typeArgs.map(toJavaType(_)).toArray + override def getActualTypeArguments: Array[Type] = typeArgs.map(toJavaType).toArray override def getRawType: Type = m.runtimeClass @@ -534,14 +541,4 @@ trait ScalaDsl { } } - /** - * Return the stack frame to allow us to identify where in a step definition file - * we are currently based - */ - private def frame: StackTraceElement = { - val frames = Thread.currentThread().getStackTrace - val currentClass = self.getClass.getName - frames.find(_.getClassName == currentClass).get - } - } diff --git a/scala/sources/src/main/scala/io/cucumber/scala/ScalaDslRegistry.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaDslRegistry.scala new file mode 100644 index 00000000..331e5b93 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaDslRegistry.scala @@ -0,0 +1,20 @@ +package io.cucumber.scala + +import io.cucumber.core.backend.{HookDefinition, StepDefinition} + +import scala.collection.mutable.ArrayBuffer + +class ScalaDslRegistry { + + val stepDefinitions = new ArrayBuffer[StepDefinition] + + val beforeHooks = new ArrayBuffer[HookDefinition] + + val beforeStepHooks = new ArrayBuffer[HookDefinition] + + val afterHooks = new ArrayBuffer[HookDefinition] + + val afterStepHooks = new ArrayBuffer[HookDefinition] + +} + diff --git a/scala/sources/src/main/scala/io/cucumber/scala/ScalaHookDefinition.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaHookDefinition.scala new file mode 100644 index 00000000..0b8208fb --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaHookDefinition.scala @@ -0,0 +1,18 @@ +package io.cucumber.scala + +import io.cucumber.core.backend.{HookDefinition, TestCaseState} +import io.cucumber.scala.Aliases.HookBody + +class ScalaHookDefinition(tagExpression: String, order: Int, body: HookBody) + extends AbstractGlueDefinition(new Exception().getStackTrace()(3)) + with HookDefinition { + + override def execute(state: TestCaseState): Unit = { + body(new Scenario(state)) + } + + override def getTagExpression: String = tagExpression + + override def getOrder: Int = order + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/ScalaParameterInfo.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaParameterInfo.scala new file mode 100644 index 00000000..0e509637 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaParameterInfo.scala @@ -0,0 +1,15 @@ +package io.cucumber.scala + +import java.lang.reflect.Type + +import io.cucumber.core.backend.{ParameterInfo, TypeResolver} + +class ScalaParameterInfo(typeResolver: ScalaTypeResolver) extends ParameterInfo { + + override def getType: Type = typeResolver.`type` + + override def isTransposed: Boolean = false + + override def getTypeResolver: TypeResolver = typeResolver + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/ScalaSnippet.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaSnippet.scala new file mode 100644 index 00000000..3e6a7e97 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaSnippet.scala @@ -0,0 +1,63 @@ +package io.cucumber.scala + +import java.lang.reflect.Type +import java.text.MessageFormat +import java.util.{Map => JMap} + +import io.cucumber.core.backend.Snippet +import io.cucumber.datatable.DataTable + +import scala.collection.JavaConverters._ + +object ScalaSnippet { + + // Allows to use """ in """xxx"""" strings + val tripleDoubleQuotes = "\"\"\"" + +} + +class ScalaSnippet extends Snippet { + + import ScalaSnippet.tripleDoubleQuotes + + override def template(): MessageFormat = { + new MessageFormat( + s"""{0}(${tripleDoubleQuotes}{1}${tripleDoubleQuotes}) '{' ({3}) => + | // {4} + | throw new ${classOf[PendingException].getName}() + |'}'""".stripMargin) + } + + override def tableHint(): String = { + """| // For automatic transformation, change DataTable to one of + | // E, List, List>, List>, Map or + | // Map>. E,K,V must be a String, Integer, Float, + | // Double, Byte, Short, Long, BigInteger or BigDecimal. + | // + | // For other transformations you can register a DataTableType.""".stripMargin + } + + override def escapePattern(pattern: String): String = pattern + + override def arguments(map: JMap[String, Type]): String = { + map.asScala + .map { case (argName, argType) => s"$argName: ${getArgType(argType)}" } + .mkString(", ") + } + + private def getArgType(argType: Type): String = { + argType match { + // Scala classes + // TODO is there a native Scala way of doing so? + case cType: Class[_] if cType == classOf[java.lang.Integer] => "Int" + case cType: Class[_] if cType == classOf[java.lang.Long] => "Long" + case cType: Class[_] if cType == classOf[java.lang.Float] => "Float" + case cType: Class[_] if cType == classOf[java.lang.Double] => "Double" + // Java behavior + case cType: Class[_] if cType == classOf[DataTable] => cType.getName + case cType: Class[_] => cType.getSimpleName + case _ => argType.toString + } + } + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/ScalaStepDefinition.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaStepDefinition.scala new file mode 100644 index 00000000..ef3d7552 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaStepDefinition.scala @@ -0,0 +1,60 @@ +package io.cucumber.scala + +import java.lang.reflect.{InvocationTargetException, Type} +import java.util.{List => JList} + +import io.cucumber.core.backend.{CucumberInvocationTargetException, ParameterInfo, StepDefinition} + +import scala.collection.JavaConverters._ +import scala.util.{Failure, Try} + +/** + * Implementation of step definition for scala. + * + * @param frame Representation of a stack frame containing information about the context in which a + * step was defined. Allows retrospective queries about the definition of a step. + * @param name The name of the step definition class, e.g. cucumber.runtime.scala.test.CukesStepDefinitions + * @param pattern The regex matcher that defines the cucumber step, e.g. /I eat (.*) cukes$/ + * @param types Parameters types of body step definition + * @param body Function body of a step definition. This is what actually runs the code within the step def. + */ +class ScalaStepDefinition(frame: StackTraceElement, + name: String, + pattern: String, + types: Array[Type], + body: List[Any] => Any) + extends AbstractGlueDefinition(frame) + with StepDefinition { + + val parametersInfo: JList[ParameterInfo] = fromTypes(types) + + private def fromTypes(types: Array[Type]): JList[ParameterInfo] = { + types + .map(new ScalaTypeResolver(_)) + .map(new ScalaParameterInfo(_)) + .toList + .asInstanceOf[List[ParameterInfo]] + .asJava + } + + override def execute(args: Array[AnyRef]): Unit = { + Try { + body(args.toList) + } + // We do this to respect the contract defined by StepDefinition + // although throwing exceptions is not very Scala-ish + .recoverWith { + case ex => Failure(new CucumberInvocationTargetException(this, new InvocationTargetException(ex))) + } + .get + + } + + override def getPattern: String = pattern + + override def parameterInfos(): JList[ParameterInfo] = parametersInfo + + // Easier to just print out fileName and lineNumber + override def getLocation(): String = frame.getFileName + ":" + frame.getLineNumber + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/ScalaTypeResolver.scala b/scala/sources/src/main/scala/io/cucumber/scala/ScalaTypeResolver.scala new file mode 100644 index 00000000..3a965118 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/ScalaTypeResolver.scala @@ -0,0 +1,14 @@ +package io.cucumber.scala + +import java.lang.reflect.Type + +import io.cucumber.core.backend.TypeResolver + +class ScalaTypeResolver(val `type`: Type) extends TypeResolver { + + override def resolve(): Type = { + // No fancy logic needed + `type` + } + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/Scenario.scala b/scala/sources/src/main/scala/io/cucumber/scala/Scenario.scala new file mode 100644 index 00000000..53660faf --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/Scenario.scala @@ -0,0 +1,37 @@ +package io.cucumber.scala + +import java.net.URI +import java.util + +import io.cucumber.core.backend.{Status, TestCaseState} + +class Scenario(val delegate: TestCaseState) { + + def getSourceTagNames: util.Collection[String] = delegate.getSourceTagNames + + def getStatus: Status = Status.valueOf(delegate.getStatus.name) + + def isFailed: Boolean = delegate.isFailed + + @deprecated + def embed(data: Array[Byte], mediaType: String): Unit = { + delegate.embed(data, mediaType) + } + + def embed(data: Array[Byte], mediaType: String, name: String): Unit = { + delegate.embed(data, mediaType, name) + } + + def write(text: String): Unit = { + delegate.write(text) + } + + def getName: String = delegate.getName + + def getId: String = delegate.getId + + def getUri: URI = delegate.getUri + + def getLine: Integer = delegate.getLine + +} diff --git a/scala/sources/src/main/scala/io/cucumber/scala/Utils.scala b/scala/sources/src/main/scala/io/cucumber/scala/Utils.scala new file mode 100644 index 00000000..693d2a45 --- /dev/null +++ b/scala/sources/src/main/scala/io/cucumber/scala/Utils.scala @@ -0,0 +1,15 @@ +package io.cucumber.scala + +private[scala] object Utils { + + /** + * Return the stack frame to allow us to identify where in a step definition file + * we are currently based + */ + def frame(self: Any): StackTraceElement = { + val frames = Thread.currentThread().getStackTrace + val currentClass = self.getClass.getName + frames.find(_.getClassName == currentClass).get + } + +} diff --git a/scala/sources/src/main/scala/cucumber/api/scala/package.html b/scala/sources/src/main/scala/io/cucumber/scala/package.html similarity index 100% rename from scala/sources/src/main/scala/cucumber/api/scala/package.html rename to scala/sources/src/main/scala/io/cucumber/scala/package.html diff --git a/scala/sources/src/test/resources/cucumber/runtime/scala/test/cukes.feature b/scala/sources/src/test/resources/tests/cukes.feature similarity index 100% rename from scala/sources/src/test/resources/cucumber/runtime/scala/test/cukes.feature rename to scala/sources/src/test/resources/tests/cukes.feature diff --git a/scala/sources/src/test/scala/cucumber/api/scala/ScalaDslTest.scala b/scala/sources/src/test/scala/cucumber/api/scala/ScalaDslTest.scala deleted file mode 100644 index 8a2d8ad1..00000000 --- a/scala/sources/src/test/scala/cucumber/api/scala/ScalaDslTest.scala +++ /dev/null @@ -1,167 +0,0 @@ -package cucumber.api.scala - -import java.util.Locale - -import _root_.org.junit.{Assert, Test} -import Assert._ -import _root_.gherkin.pickles.PickleTag - -import collection.JavaConverters._ -import cucumber.api.Scenario -import io.cucumber.stepexpression.TypeRegistry - -class ScalaDslTest { - - object StubScenario extends Scenario{ - def getSourceTagNames = null - - def getStatus = null - - def isFailed = false - - def embed(p1: Array[Byte], p2: String) {} - - def embed(p1: Array[Byte], p2: String, p3: String) {} - - def write(p1: String) {} - - def getName = "" - - def getId = "" - - def getUri = "" - - def getLines = null - - } - - @Test - def emptyBefore { - - var actualScenario : Scenario = null - - object Befores extends ScalaDsl with EN { - Before { actualScenario = _ } - } - - assertEquals(1, Befores.beforeHooks.size) - val hook = Befores.beforeHooks.head - assertTrue(hook.matches(List[PickleTag]().asJava)) - hook.execute(StubScenario) - assertEquals(Int.MaxValue, hook.getOrder) - assertEquals(StubScenario, actualScenario) - } - - @Test - def taggedBefore { - var actualScenario : Scenario = null - - object Befores extends ScalaDsl with EN { - Before("(@foo or @bar) and @zap"){ actualScenario = _ } - } - - assertEquals(1, Befores.beforeHooks.size) - - val hook = Befores.beforeHooks.head - assertFalse(hook.matches(List[PickleTag]().asJava)) - assertTrue(hook.matches(List(new PickleTag(null, "@bar"), new PickleTag(null, "@zap")).asJava)) - assertFalse(hook.matches(List(new PickleTag(null, "@bar")).asJava)) - - hook.execute(StubScenario) - assertEquals(StubScenario, actualScenario) - assertEquals(Int.MaxValue, hook.getOrder) - } - - @Test - def orderedBefore { - - object Befores extends ScalaDsl with EN { - Before(10){ scenario : Scenario => } - } - - val hook = Befores.beforeHooks(0) - assertEquals(10, hook.getOrder) - } - - @Test - def taggedOrderedBefore { - - object Befores extends ScalaDsl with EN { - Before(10, "(@foo or @bar) and @zap"){ scenario : Scenario => } - } - - val hook = Befores.beforeHooks(0) - assertEquals(10, hook.getOrder) - } - - @Test - def emptyAfter { - - var actualScenario : Scenario = null - - object Afters extends ScalaDsl with EN { - After { actualScenario = _ } - } - - assertEquals(1, Afters.afterHooks.size) - val hook = Afters.afterHooks.head - assertTrue(hook.matches(List[PickleTag]().asJava)) - hook.execute(StubScenario) - assertEquals(StubScenario, actualScenario) - } - - @Test - def taggedAfter { - var actualScenario : Scenario = null - - object Afters extends ScalaDsl with EN { - After("(@foo or @bar) and @zap"){ actualScenario = _ } - } - - assertEquals(1, Afters.afterHooks.size) - - val hook = Afters.afterHooks.head - assertFalse(hook.matches(List[PickleTag]().asJava)) - assertTrue(hook.matches(List(new PickleTag(null, "@bar"), new PickleTag(null, "@zap")).asJava)) - assertFalse(hook.matches(List(new PickleTag(null, "@bar")).asJava)) - - hook.execute(StubScenario) - assertEquals(StubScenario, actualScenario) - } - - @Test - def noArg { - var called = false - - object Dummy extends ScalaDsl with EN { - Given("x") { called = true } - } - - assertEquals(1, Dummy.getStepDefs(new TypeRegistry(Locale.ENGLISH)).size) - val step = Dummy.getStepDefs(new TypeRegistry(Locale.ENGLISH)).head - assertEquals("ScalaDslTest.scala:137", step.getLocation(true)) // be careful with formatting or this test will break - assertEquals("x", step.getPattern) - step.execute( Array()) - assertTrue(called) - } - - @Test - def args { - var thenumber = 0 - var thecolour = "" - - object Dummy extends ScalaDsl with EN { - Given("Oh boy, (\\d+) (\\s+) cukes"){ (num:Int, colour:String) => - thenumber = num - thecolour = colour - } - } - - assertEquals(1, Dummy.getStepDefs(new TypeRegistry(Locale.ENGLISH)).size) - val step = Dummy.getStepDefs(new TypeRegistry(Locale.ENGLISH))(0) - step.execute(Array(new java.lang.Integer(5), "green")) - assertEquals(5, thenumber) - assertEquals("green", thecolour) - } - -} diff --git a/scala/sources/src/test/scala/cucumber/runtime/scala/test/RunCukesTest.scala b/scala/sources/src/test/scala/cucumber/runtime/scala/test/RunCukesTest.scala deleted file mode 100644 index 492433b8..00000000 --- a/scala/sources/src/test/scala/cucumber/runtime/scala/test/RunCukesTest.scala +++ /dev/null @@ -1,9 +0,0 @@ -package cucumber.runtime.scala.test - -import _root_.org.junit.runner.RunWith -import _root_.cucumber.api.junit.Cucumber -import _root_.cucumber.api.CucumberOptions - -@RunWith(classOf[Cucumber]) -@CucumberOptions(strict=true) -class RunCukesTest diff --git a/scala/sources/src/test/scala/io/cucumber/scala/ScalaBackendTest.scala b/scala/sources/src/test/scala/io/cucumber/scala/ScalaBackendTest.scala new file mode 100644 index 00000000..190d2d98 --- /dev/null +++ b/scala/sources/src/test/scala/io/cucumber/scala/ScalaBackendTest.scala @@ -0,0 +1,37 @@ +package io.cucumber.scala + +import java.net.URI +import java.util.function.Supplier + +import io.cucumber.core.backend.Glue +import org.junit.Assert.{assertEquals, assertTrue} +import org.junit.{Before, Test} +import org.mockito.Mockito.mock + +import scala.collection.JavaConverters._ + +class ScalaBackendTest { + + private val glue: Glue = mock(classOf[Glue]) + + private var backend: ScalaBackend = _ + + @Before + def createBackend() = { + // Note: keep unnecessary code for Scala 2.11 compat + val classLoaderSupplier: Supplier[ClassLoader] = new Supplier[ClassLoader] { + override def get(): ClassLoader = Thread.currentThread().getContextClassLoader + } + backend = new ScalaBackend(classLoaderSupplier) + } + + @Test + def finds_step_definitions_by_classpath_url() = { + backend.loadGlue(glue, List(URI.create("classpath:io/cucumber/scala/steps")).asJava) + backend.buildWorld() + + assertEquals(1, backend.scalaGlueInstances.size) + assertTrue(backend.scalaGlueInstances.head.isInstanceOf[io.cucumber.scala.steps.Steps]) + } + +} diff --git a/scala/sources/src/test/scala/io/cucumber/scala/ScalaDslTest.scala b/scala/sources/src/test/scala/io/cucumber/scala/ScalaDslTest.scala new file mode 100644 index 00000000..5cae2a92 --- /dev/null +++ b/scala/sources/src/test/scala/io/cucumber/scala/ScalaDslTest.scala @@ -0,0 +1,181 @@ +package io.cucumber.scala + +import java.net.URI + +import io.cucumber.core.backend.TestCaseState +import org.junit.Assert.{assertEquals, assertTrue} +import org.junit.Test + +class ScalaDslTest { + + val testCaseTest: TestCaseState = new TestCaseState { + override def getSourceTagNames = null + + override def getStatus = null + + override def isFailed = false + + override def embed(p1: Array[Byte], p2: String) {} + + override def embed(p1: Array[Byte], p2: String, p3: String) {} + + override def write(p1: String) {} + + override def getName = "" + + override def getId = "" + + override def getUri = new URI("classpath:plop") + + override def getLine: Integer = null + } + + @Test + def emptyBefore { + + var actualScenario: Scenario = null + + object Befores extends ScalaDsl with EN { + Before { + actualScenario = _ + } + } + + assertEquals(1, Befores.registry.beforeHooks.size) + + val hook = Befores.registry.beforeHooks.head + hook.execute(testCaseTest) + + assertEquals(testCaseTest, actualScenario.delegate) + assertEquals(1000, hook.getOrder) + assertEquals("", hook.getTagExpression) + } + + @Test + def taggedBefore { + var actualScenario: Scenario = null + + object Befores extends ScalaDsl with EN { + Before("(@foo or @bar) and @zap") { + actualScenario = _ + } + } + + assertEquals(1, Befores.registry.beforeHooks.size) + + val hook = Befores.registry.beforeHooks.head + hook.execute(testCaseTest) + + assertEquals(testCaseTest, actualScenario.delegate) + assertEquals(1000, hook.getOrder) + assertEquals("(@foo or @bar) and @zap", hook.getTagExpression) + } + + @Test + def orderedBefore { + + object Befores extends ScalaDsl with EN { + Before(10) { scenario: Scenario => } + } + + val hook = Befores.registry.beforeHooks(0) + assertEquals(10, hook.getOrder) + assertEquals("", hook.getTagExpression) + } + + @Test + def taggedOrderedBefore { + + object Befores extends ScalaDsl with EN { + Before("(@foo or @bar) and @zap", 10) { scenario: Scenario => } + } + + val hook = Befores.registry.beforeHooks(0) + assertEquals(10, hook.getOrder) + assertEquals("(@foo or @bar) and @zap", hook.getTagExpression) + } + + @Test + def emptyAfter { + + var actualScenario: Scenario = null + + object Afters extends ScalaDsl with EN { + After { + actualScenario = _ + } + } + + assertEquals(1, Afters.registry.afterHooks.size) + + val hook = Afters.registry.afterHooks.head + hook.execute(testCaseTest) + + assertEquals(testCaseTest, actualScenario.delegate) + assertEquals(1000, hook.getOrder) + assertEquals("", hook.getTagExpression) + } + + @Test + def taggedAfter { + var actualScenario: Scenario = null + + object Afters extends ScalaDsl with EN { + After("(@foo or @bar) and @zap") { + actualScenario = _ + } + } + + assertEquals(1, Afters.registry.afterHooks.size) + + val hook = Afters.registry.afterHooks.head + hook.execute(testCaseTest) + + assertEquals(testCaseTest, actualScenario.delegate) + assertEquals(1000, hook.getOrder) + assertEquals("(@foo or @bar) and @zap", hook.getTagExpression) + } + + @Test + def noArg { + var called = false + + object Dummy extends ScalaDsl with EN { + // One line to avoid difference in lineNumber between Scala versions + Given("x") { called = true } + } + + assertEquals(1, Dummy.registry.stepDefinitions.size) + + val step = Dummy.registry.stepDefinitions.head + + assertEquals("ScalaDslTest.scala:145", step.getLocation) // be careful with formatting or this test will break + assertEquals("x", step.getPattern) + + step.execute(Array()) + + assertTrue(called) + } + + @Test + def args { + var thenumber = 0 + var thecolour = "" + + object Dummy extends ScalaDsl with EN { + Given("Oh boy, (\\d+) (\\s+) cukes") { (num: Int, colour: String) => + thenumber = num + thecolour = colour + } + } + + assertEquals(1, Dummy.registry.stepDefinitions.size) + + val step = Dummy.registry.stepDefinitions.head + step.execute(Array(new java.lang.Integer(5), "green")) + + assertEquals(5, thenumber) + assertEquals("green", thecolour) + } + +} diff --git a/scala/sources/src/test/scala/io/cucumber/scala/ScalaHookDefinitionTest.scala b/scala/sources/src/test/scala/io/cucumber/scala/ScalaHookDefinitionTest.scala new file mode 100644 index 00000000..b615ac03 --- /dev/null +++ b/scala/sources/src/test/scala/io/cucumber/scala/ScalaHookDefinitionTest.scala @@ -0,0 +1,19 @@ +package io.cucumber.scala + +import org.junit.Assert.assertTrue +import org.junit.Test + +class ScalaHookDefinitionTest { + + @Test + def can_create_with_single_scenario_argument() = { + var invoked = false + val body = (scenario: Scenario) => { + invoked = true + } + val definition = new ScalaHookDefinition("", 0, body) + definition.execute(null) + assertTrue(invoked) + } + +} diff --git a/scala/sources/src/test/scala/io/cucumber/scala/ScalaSnippetTest.scala b/scala/sources/src/test/scala/io/cucumber/scala/ScalaSnippetTest.scala new file mode 100644 index 00000000..b539f727 --- /dev/null +++ b/scala/sources/src/test/scala/io/cucumber/scala/ScalaSnippetTest.scala @@ -0,0 +1,350 @@ +package io.cucumber.scala + +import java.util +import java.util.Locale + +import io.cucumber.core.gherkin.Step +import io.cucumber.core.snippets.{SnippetGenerator, SnippetType} +import io.cucumber.cucumberexpressions.{CaptureGroupTransformer, ParameterType, ParameterTypeRegistry, TypeReference} +import org.junit.Assert.assertEquals +import org.junit.Test + +import scala.collection.JavaConverters._ + +class ScalaSnippetTest { + + private val snippetType = SnippetType.UNDERSCORE + + @Test + def generatesPlainSnippet() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have {int} cukes in my {string} belly${ScalaSnippet.tripleDoubleQuotes}) { (int1: Int, string: String) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("I have 4 cukes in my \"big\" belly")) + } + + @Test + def generatesPlainSnippetUsingCustomParameterTypes() = { + val customParameterType = new ParameterType[Size]( + "size", + "small|medium|large", + classOf[Size], + new CaptureGroupTransformer[Size] { + override def transform(groups: Array[String]): Size = null + }, + true, + false + ) + + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have {double} cukes in my {size} belly${ScalaSnippet.tripleDoubleQuotes}) { (double1: Double, size: Size) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + + assertEquals(expected, snippetFor("I have 4.2 cukes in my large belly", customParameterType)) + } + + @Test + def generatesPlainSnippetUsingComplexParameterTypes() = { + val customParameterType = new ParameterType[Size]( + "sizes", + List("(small|medium|large)(( and |, )(small|medium|large))*").asJava, + new TypeReference[util.List[Size]]() {}.getType, + new CaptureGroupTransformer[Size] { + override def transform(groups: Array[String]): Size = null + }, + true, + false + ) + + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have {sizes} bellies${ScalaSnippet.tripleDoubleQuotes}) { (sizes: java.util.List) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + + assertEquals(expected, snippetFor("I have large and small bellies", customParameterType)) + } + + @Test + def generatesCopyPasteReadyStepSnippetForNumberParameters() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}before {int} after${ScalaSnippet.tripleDoubleQuotes}) { (int1: Int) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("before 5 after")) + } + + @Test + def generatesCopyPasteReadySnippetWhenStepHasIllegalJavaIdentifierChars() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have {int} cukes in: my {string} red-belly!${ScalaSnippet.tripleDoubleQuotes}) { (int1: Int, string: String) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("I have 4 cukes in: my \"big\" red-belly!")) + } + + @Test + def generatesCopyPasteReadySnippetWhenStepHasIntegersInsideStringParameter() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}the DI system receives a message saying {string}${ScalaSnippet.tripleDoubleQuotes}) { (string: String) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("the DI system receives a message saying \"{ dataIngestion: { feeds: [ feed: { merchantId: 666, feedId: 1, feedFileLocation: feed.csv } ] }\"")) + } + + @Test + def generatesSnippetWithQuestionMarks() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}is there an error?:${ScalaSnippet.tripleDoubleQuotes}) { () => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("is there an error?:")) + } + + @Test + def generatesSnippetWithLotsOfNonIdentifierCharacters() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}\\([a-z]*)?.+${ScalaSnippet.tripleDoubleQuotes}) { () => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("([a-z]*)?.+")) + } + + @Test + def generatesSnippetWithParentheses() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have {int} cukes \\(maybe more)${ScalaSnippet.tripleDoubleQuotes}) { (int1: Int) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("I have 5 cukes (maybe more)")) + } + + @Test + def generatesSnippetWithBrackets() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have {int} cukes [maybe more]${ScalaSnippet.tripleDoubleQuotes}) { (int1: Int) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("I have 5 cukes [maybe more]")) + } + + @Test + def generatesSnippetWithDocString() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have:${ScalaSnippet.tripleDoubleQuotes}) { (docString: String) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetForDocString("I have:", "hello")) + } + + @Test + def generatesSnippetWithMultipleArgumentsNamedDocString() = { + val customParameterType = new ParameterType[String]( + "docString", + "\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\"", + classOf[String], + new CaptureGroupTransformer[String] { + override def transform(strings: Array[String]): String = null + }, + true, + false + ) + + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have a {docString}:${ScalaSnippet.tripleDoubleQuotes}) { (docString: String, docString1: String) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |} + |Given(${ScalaSnippet.tripleDoubleQuotes}I have a {string}:${ScalaSnippet.tripleDoubleQuotes}) { (string: String, docString: String) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetForDocString("I have a \"Documentation String\":", "hello", customParameterType)) + } + + @Test + def generatesSnippetWithDataTable() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have:${ScalaSnippet.tripleDoubleQuotes}) { (dataTable: io.cucumber.datatable.DataTable) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetForDataTable("I have:")) + } + + @Test + def generatesSnippetWithMultipleArgumentsNamedDataTable() = { + val customParameterType = new ParameterType[String]( + "dataTable", + "\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\"", + classOf[String], + new CaptureGroupTransformer[String] { + override def transform(strings: Array[String]): String = null + }, + true, + false + ) + + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have in table {dataTable}:${ScalaSnippet.tripleDoubleQuotes}) { (dataTable: String, dataTable1: io.cucumber.datatable.DataTable) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |} + |Given(${ScalaSnippet.tripleDoubleQuotes}I have in table {string}:${ScalaSnippet.tripleDoubleQuotes}) { (string: String, dataTable: io.cucumber.datatable.DataTable) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetForDataTable("I have in table \"M6\":", customParameterType)) + } + + @Test + def generateSnippetWithOutlineParam() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}Then it responds ${ScalaSnippet.tripleDoubleQuotes}) { () => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetFor("Then it responds ")) + } + + @Test + def generatesSnippetUsingFirstGivenWhenThenKeyWord() = { + val expected = + s"""When(${ScalaSnippet.tripleDoubleQuotes}I have {int} cukes in my {string} belly${ScalaSnippet.tripleDoubleQuotes}) { (int1: Int, string: String) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetForWhenAnd("I have 4 cukes in my \"big\" belly")) + } + + @Test + def generatesSnippetDefaultsToGiven() = { + val expected = + s"""Given(${ScalaSnippet.tripleDoubleQuotes}I have {int} cukes in my {string} belly${ScalaSnippet.tripleDoubleQuotes}) { (int1: Int, string: String) => + | // Write code here that turns the phrase above into concrete actions + | throw new io.cucumber.scala.PendingException() + |}""".stripMargin + assertEquals(expected, snippetForWildCard("I have 4 cukes in my \"big\" belly")) + } + + private def getSnippet(step: Step, parameterType: Option[ParameterType[_]] = None): String = { + val parameterTypeRegistry = new ParameterTypeRegistry(Locale.ENGLISH) + parameterType.foreach { pt => + parameterTypeRegistry.defineParameterType(pt) + } + val snippet = new SnippetGenerator(new ScalaSnippet, parameterTypeRegistry).getSnippet(step, snippetType).asScala + snippet.mkString("\n") + } + + private def snippetFor(stepText: String): String = { + val step = createStep(stepText) + getSnippet(step) + } + + private def snippetFor(stepText: String, parameterType: ParameterType[_]): String = { + val step = createStep(stepText) + getSnippet(step, Some(parameterType)) + } + + private def snippetForDocString(stepText: String, docString: String): String = { + val step = createStepWithDocString(stepText, docString) + getSnippet(step) + } + + private def snippetForDocString(stepText: String, docString: String, parameterType: ParameterType[_]): String = { + val step = createStepWithDocString(stepText, docString) + getSnippet(step, Some(parameterType)) + } + + private def snippetForDataTable(stepText: String): String = { + val step = createStepWithDataTable(stepText) + getSnippet(step) + } + + private def snippetForDataTable(stepText: String, parameterType: ParameterType[_]): String = { + val step = createStepWithDataTable(stepText) + getSnippet(step, Some(parameterType)) + } + + private def snippetForWhenAnd(stepText: String): String = { + val source = + s"""Feature: Test feature + | Scenario: Test Scenario + | When some other step + | And ${stepText} + |""".stripMargin + + val feature = TestFeatureParser.parse(source) + val step = feature.getPickles.asScala.head.getSteps.asScala(1) + + getSnippet(step) + } + + private def snippetForWildCard(stepText: String): String = { + val source = + s"""Feature: Test feature + | Scenario: Test Scenario + | * ${stepText} + |""".stripMargin + + val feature = TestFeatureParser.parse(source) + val step = feature.getPickles.asScala.head.getSteps.asScala.head + + getSnippet(step) + } + + private def createStep(stepText: String): Step = { + val source = + s"""Feature: Test feature + | Scenario: Test Scenario + | Given ${stepText} + |""".stripMargin + + val feature = TestFeatureParser.parse(source) + feature.getPickles.asScala.head.getSteps.asScala.head + } + + private def createStepWithDocString(stepText: String, docString: String): Step = { + val source = + s"""Feature: Test feature + | Scenario: Test Scenario + | Given ${stepText} + | ${ScalaSnippet.tripleDoubleQuotes} + | ${docString} + | ${ScalaSnippet.tripleDoubleQuotes} + |""".stripMargin + + val feature = TestFeatureParser.parse(source) + feature.getPickles.asScala.head.getSteps.asScala.head + } + + private def createStepWithDataTable(stepText: String): Step = { + val source = + s"""Feature: Test feature + | Scenario: Test Scenario + | Given ${stepText} + | | key | + | | value | + |""".stripMargin + + val feature = TestFeatureParser.parse(source) + feature.getPickles.asScala.head.getSteps.asScala.head + } + + private class Size { + // Dummy. Makes the test readable + } + +} diff --git a/scala/sources/src/test/scala/io/cucumber/scala/ScalaStepDefinitionTest.scala b/scala/sources/src/test/scala/io/cucumber/scala/ScalaStepDefinitionTest.scala new file mode 100644 index 00000000..31ce2767 --- /dev/null +++ b/scala/sources/src/test/scala/io/cucumber/scala/ScalaStepDefinitionTest.scala @@ -0,0 +1,46 @@ +package io.cucumber.scala + +import io.cucumber.core.backend.CucumberInvocationTargetException +import org.junit.Test +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +import scala.util.Try + +class ScalaStepDefinitionTest { + + @Test + def can_define_step() = { + var args: List[Any] = List() + val body = (params: List[Any]) => { + args = params + } + val definition = new ScalaStepDefinition(null, "whatever", "three (.*) mice", Array(classOf[String]), body) + definition.execute(Array("one_string_argument")) + + assertEquals("one_string_argument", args.head) + } + + @Test + def can_provide_location_of_step() = { + val body = (_: List[Any]) => { + throw new PendingException() + } + val frame = Utils.frame(this) + val definition = new ScalaStepDefinition(frame, "whatever", "three (.*) mice", Array(), body) + + val tried = Try(definition.execute(Array())) + + assertTrue(tried.isFailure) + val ex = tried.failed.get + assertTrue(ex.isInstanceOf[CucumberInvocationTargetException]) + val matched = ex.asInstanceOf[CucumberInvocationTargetException].getInvocationTargetExceptionCause.getStackTrace.filter(definition.isDefinedAt).head + + // Method name in Scala does not make much sense + //assertEquals("$anonfun$can_provide_location_of_step$1", matched.getMethodName) + //assertEquals(classOf[ScalaStepDefinitionTest].getName, matched.getClassName) + assertEquals("ScalaStepDefinitionTest.scala", matched.getFileName) + assertEquals(27, matched.getLineNumber) + } + +} diff --git a/scala/sources/src/test/scala/io/cucumber/scala/TestFeatureParser.scala b/scala/sources/src/test/scala/io/cucumber/scala/TestFeatureParser.scala new file mode 100644 index 00000000..cc692acd --- /dev/null +++ b/scala/sources/src/test/scala/io/cucumber/scala/TestFeatureParser.scala @@ -0,0 +1,35 @@ +package io.cucumber.scala + +import java.io.{ByteArrayInputStream, InputStream} +import java.net.URI +import java.nio.charset.StandardCharsets +import java.util.UUID +import java.util.function.Supplier + +import io.cucumber.core.feature.{FeatureIdentifier, FeatureParser} +import io.cucumber.core.gherkin.Feature +import io.cucumber.core.resource.Resource + +object TestFeatureParser { + + def parse(source: String): Feature = { + parse("file:test.feature", source) + } + + def parse(uri: String, source: String): Feature = { + parse(FeatureIdentifier.parse(uri), source) + } + + def parse(uri: URI, source: String): Feature = { + // Keep unnecessary code for compat with Scala 2.11 + val supplier = new Supplier[UUID] { + override def get(): UUID = UUID.randomUUID + } + new FeatureParser(supplier).parseResource(new Resource { + override def getUri: URI = uri + + override def getInputStream: InputStream = new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8)) + }).orElse(null) + } + +} diff --git a/scala/sources/src/test/scala/io/cucumber/scala/steps/Steps.scala b/scala/sources/src/test/scala/io/cucumber/scala/steps/Steps.scala new file mode 100644 index 00000000..1ad34921 --- /dev/null +++ b/scala/sources/src/test/scala/io/cucumber/scala/steps/Steps.scala @@ -0,0 +1,11 @@ +package io.cucumber.scala.steps + +import io.cucumber.scala.{EN, ScalaDsl} + +class Steps extends ScalaDsl with EN { + + Given("""test""") { () => + // Nothing + } + +} diff --git a/scala/sources/src/test/scala/tests/RunCukesTest.scala b/scala/sources/src/test/scala/tests/RunCukesTest.scala new file mode 100644 index 00000000..8cf0ea8d --- /dev/null +++ b/scala/sources/src/test/scala/tests/RunCukesTest.scala @@ -0,0 +1,8 @@ +package tests + +import io.cucumber.junit.{Cucumber, CucumberOptions} +import org.junit.runner.RunWith + +@RunWith(classOf[Cucumber]) +@CucumberOptions(strict = true) +class RunCukesTest diff --git a/scala/sources/src/test/scala/cucumber/runtime/scala/test/StepDefs.scala b/scala/sources/src/test/scala/tests/StepDefs.scala similarity index 90% rename from scala/sources/src/test/scala/cucumber/runtime/scala/test/StepDefs.scala rename to scala/sources/src/test/scala/tests/StepDefs.scala index 719ff5d5..cdc5b75a 100644 --- a/scala/sources/src/test/scala/cucumber/runtime/scala/test/StepDefs.scala +++ b/scala/sources/src/test/scala/tests/StepDefs.scala @@ -1,17 +1,17 @@ -package cucumber.runtime.scala.test +package tests -import _root_.cucumber.api.scala._ -import junit.framework.Assert._ - -import scala.collection.JavaConverters._ -import cucumber.runtime.scala.model.{Cukes, Person, Snake} import java.util.{List => JList, Map => JMap} import io.cucumber.datatable.DataTable +import io.cucumber.scala.{EN, ScalaDsl} +import junit.framework.Assert._ +import tests.model.{Cukes, Person, Snake} + +import scala.collection.JavaConverters._ /** - * Test step definitions to exercise Scala cucumber - */ + * Test step definitions to exercise Scala cucumber + */ class CukesStepDefinitions extends ScalaDsl with EN { Given("""I have {} {string} in my belly""") { (howMany: Int, what: String) => @@ -127,8 +127,8 @@ class CukesStepDefinitions extends ScalaDsl with EN { assertEquals(true, boolBelly) } - Given("""I have a table the sum of all rows should be {int} :""") { (value: Int, table: DataTable) => - assertEquals(value, table.asList(classOf[String]).asScala.drop(1).map(String.valueOf(_:String).toInt).foldLeft(0)(_ + _)) + Given("""I have a table the sum of all rows should be {int} :""") { (value: Int, table: DataTable) => + assertEquals(value, table.asList(classOf[String]).asScala.drop(1).map(String.valueOf(_: String).toInt).foldLeft(0)(_ + _)) } var snake: Snake = null @@ -165,11 +165,11 @@ class CukesStepDefinitions extends ScalaDsl with EN { assertEquals(colors, cukes.asScala.map(_.color).mkString(", ")) } - var gin : Int = 13 - var vermouth : Int = 42 - var maritinis : Int = 0 + var gin: Int = 13 + var vermouth: Int = 42 + var maritinis: Int = 0 - Given("^I drink gin and vermouth$"){ () => + Given("^I drink gin and vermouth$") { () => gin = 13 vermouth = 42 } diff --git a/scala/sources/src/test/scala/cucumber/runtime/scala/test/TypeRegistryConfiguration.scala b/scala/sources/src/test/scala/tests/TypeRegistryConfiguration.scala similarity index 79% rename from scala/sources/src/test/scala/cucumber/runtime/scala/test/TypeRegistryConfiguration.scala rename to scala/sources/src/test/scala/tests/TypeRegistryConfiguration.scala index cb363701..fa705064 100644 --- a/scala/sources/src/test/scala/cucumber/runtime/scala/test/TypeRegistryConfiguration.scala +++ b/scala/sources/src/test/scala/tests/TypeRegistryConfiguration.scala @@ -1,115 +1,112 @@ -package cucumber.runtime.scala.test - -import java.lang.reflect.Type -import java.util -import java.util.Locale -import cucumber.api.{TypeRegistry, TypeRegistryConfigurer} -import cucumber.runtime.scala.model.{Cukes, Person, Snake} -import io.cucumber.cucumberexpressions.{ParameterByTypeTransformer, ParameterType, Transformer} -import io.cucumber.datatable.dependency.com.fasterxml.jackson.databind.ObjectMapper -import io.cucumber.datatable.{DataTableType, TableCellByTypeTransformer, TableEntryByTypeTransformer, TableEntryTransformer} - -class TypeRegistryConfiguration extends TypeRegistryConfigurer { - - /** - * Transforms an ASCII snake into an object, for example: - * - * {{{ - * ====> becomes Snake(length = 5, direction = 'east) - * ==> becomes Snake(length = 3, direction = 'east) - * }}} - */ - private val snakeTransformer = new Transformer[Snake]() { - def transform(s: String) = { - val size = s.size - val direction = s.toList match { - case '<' :: _ => 'west - case l if l.last == '>' => 'east - } - Snake(size, direction) - } - } - - private val personTransformer = new Transformer[Person]() { - def transform(s: String) = { - Person(s) - } - } - - private val booleanTransformer = new Transformer[Boolean]() { - def transform(s: String) = { - s.trim.equals("true") - } - } - - private val charTransformer = new Transformer[Char]() { - def transform(s: String) = { - s.charAt(0) - } - } - - private val listTransformer =new TableEntryTransformer[Cukes]() { - override def transform(map: util.Map[String, String]): Cukes = { - new Cukes(map.get("Number").toInt, map.get("Color")) - } - } - - override def locale(): Locale = Locale.ENGLISH - - override def configureTypeRegistry(typeRegistry: TypeRegistry): Unit = { - val defaultTransformer = new DefaultTransformer() - typeRegistry.setDefaultDataTableCellTransformer(defaultTransformer) - typeRegistry.setDefaultDataTableEntryTransformer(defaultTransformer) - typeRegistry.setDefaultParameterTransformer(defaultTransformer) - - typeRegistry.defineParameterType(new ParameterType[Snake]( - "snake", - "[=><]+", - classOf[Snake], - snakeTransformer - )) - - typeRegistry.defineParameterType(new ParameterType[Person]( - "person", - ".+", - classOf[Person], - personTransformer - )) - - typeRegistry.defineParameterType(new ParameterType[Boolean]( - "boolean", - "true|false", - classOf[Boolean], - booleanTransformer - )) - - typeRegistry.defineParameterType(new ParameterType[Char]( - "char", - ".", - classOf[Char], - charTransformer - )) - - typeRegistry.defineDataTableType(new DataTableType(classOf[Cukes],listTransformer)) - } - private class DefaultTransformer - extends ParameterByTypeTransformer - with TableEntryByTypeTransformer - with TableCellByTypeTransformer { - - var objectMapper: ObjectMapper = new ObjectMapper() - - override def transform(s: String, `type`: Type): AnyRef = - objectMapper.convertValue(s, objectMapper.constructType(`type`)) - - - override def transform[T](s: String, aClass: Class[T]): T = - objectMapper.convertValue(s, aClass) - - override def transform[T](map: util.Map[String, String], - aClass: Class[T], - tableCellByTypeTransformer: TableCellByTypeTransformer): T = - objectMapper.convertValue(map, aClass) - } - -} +package tests + +import java.lang.reflect.Type +import java.util +import java.util.{Locale, Map => JMap} + +import com.fasterxml.jackson.databind.ObjectMapper +import io.cucumber.core.api.{TypeRegistry, TypeRegistryConfigurer} +import io.cucumber.cucumberexpressions.{ParameterByTypeTransformer, ParameterType, Transformer} +import io.cucumber.datatable.{DataTableType, TableCellByTypeTransformer, TableEntryByTypeTransformer, TableEntryTransformer} +import tests.model.{Cukes, Person, Snake} + +class TypeRegistryConfiguration extends TypeRegistryConfigurer { + + /** + * Transforms an ASCII snake into an object, for example: + * + * {{{ + * ====> becomes Snake(length = 5, direction = 'east) + * ==> becomes Snake(length = 3, direction = 'east) + * }}} + */ + private val snakeTransformer = new Transformer[Snake]() { + def transform(s: String) = { + val size = s.size + val direction = s.toList match { + case '<' :: _ => 'west + case l if l.last == '>' => 'east + } + Snake(size, direction) + } + } + + private val personTransformer = new Transformer[Person]() { + def transform(s: String) = { + Person(s) + } + } + + private val booleanTransformer = new Transformer[Boolean]() { + def transform(s: String) = { + s.trim.equals("true") + } + } + + private val charTransformer = new Transformer[Char]() { + def transform(s: String) = { + s.charAt(0) + } + } + + private val listTransformer =new TableEntryTransformer[Cukes]() { + override def transform(map: util.Map[String, String]): Cukes = { + new Cukes(map.get("Number").toInt, map.get("Color")) + } + } + + override def locale(): Locale = Locale.ENGLISH + + override def configureTypeRegistry(typeRegistry: TypeRegistry): Unit = { + val defaultTransformer = new DefaultTransformer() + typeRegistry.setDefaultDataTableCellTransformer(defaultTransformer) + typeRegistry.setDefaultDataTableEntryTransformer(defaultTransformer) + typeRegistry.setDefaultParameterTransformer(defaultTransformer) + + typeRegistry.defineParameterType(new ParameterType[Snake]( + "snake", + "[=><]+", + classOf[Snake], + snakeTransformer + )) + + typeRegistry.defineParameterType(new ParameterType[Person]( + "person", + ".+", + classOf[Person], + personTransformer + )) + + typeRegistry.defineParameterType(new ParameterType[Boolean]( + "boolean", + "true|false", + classOf[Boolean], + booleanTransformer + )) + + typeRegistry.defineParameterType(new ParameterType[Char]( + "char", + ".", + classOf[Char], + charTransformer + )) + + typeRegistry.defineDataTableType(new DataTableType(classOf[Cukes],listTransformer)) + } + private class DefaultTransformer + extends ParameterByTypeTransformer + with TableEntryByTypeTransformer + with TableCellByTypeTransformer { + + var objectMapper: ObjectMapper = new ObjectMapper() + + override def transform(s: String, `type`: Type): AnyRef = + objectMapper.convertValue(s, objectMapper.constructType(`type`)) + + override def transform(map: JMap[String, String], `type`: Type, tableCellByTypeTransformer: TableCellByTypeTransformer): AnyRef = { + objectMapper.convertValue(map, objectMapper.constructType(`type`)) + } + + } + +} diff --git a/scala/sources/src/test/scala/cucumber/runtime/scala/model/Cuke.scala b/scala/sources/src/test/scala/tests/model/Cuke.scala similarity index 55% rename from scala/sources/src/test/scala/cucumber/runtime/scala/model/Cuke.scala rename to scala/sources/src/test/scala/tests/model/Cuke.scala index abcf712d..16db2353 100644 --- a/scala/sources/src/test/scala/cucumber/runtime/scala/model/Cuke.scala +++ b/scala/sources/src/test/scala/tests/model/Cuke.scala @@ -1,3 +1,3 @@ -package cucumber.runtime.scala.model +package tests.model case class Cukes(number: Int, color: String) diff --git a/scala/sources/src/test/scala/cucumber/runtime/scala/model/Person.scala b/scala/sources/src/test/scala/tests/model/Person.scala similarity index 80% rename from scala/sources/src/test/scala/cucumber/runtime/scala/model/Person.scala rename to scala/sources/src/test/scala/tests/model/Person.scala index e6b24923..5a9c4fb5 100644 --- a/scala/sources/src/test/scala/cucumber/runtime/scala/model/Person.scala +++ b/scala/sources/src/test/scala/tests/model/Person.scala @@ -1,4 +1,4 @@ -package cucumber.runtime.scala.model +package tests.model /** * Test model for a "Person" diff --git a/scala/sources/src/test/scala/cucumber/runtime/scala/model/Snake.scala b/scala/sources/src/test/scala/tests/model/Snake.scala similarity index 86% rename from scala/sources/src/test/scala/cucumber/runtime/scala/model/Snake.scala rename to scala/sources/src/test/scala/tests/model/Snake.scala index ae7dde3f..59064596 100644 --- a/scala/sources/src/test/scala/cucumber/runtime/scala/model/Snake.scala +++ b/scala/sources/src/test/scala/tests/model/Snake.scala @@ -1,4 +1,4 @@ -package cucumber.runtime.scala.model +package tests.model /** * Test model "Snake" to exercise the custom mapper functionality