Skip to content

Implement BeforeAll and AfterAll hooks (Cucumber Core 7.x) #234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Check out the [Upgrade Guide](docs/upgrade_v8.md).

### Added

- [Scala] Added `BeforeAll` and `AfterAll` hooks. See [Hooks](docs/hooks.md).

### Changed

- [Core] Updated `cucumber-core` dependency to [7.0.0-RC1](https://github.com/cucumber/cucumber-jvm/blob/main/CHANGELOG.md)
Expand Down
2 changes: 2 additions & 0 deletions cucumber-scala/src/main/scala/io/cucumber/scala/Aliases.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package io.cucumber.scala
*/
object Aliases {

type StaticHookDefinitionBody = () => Unit

type HookDefinitionBody = Scenario => Unit

type StepDefinitionBody = () => Unit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,25 @@ class GlueAdaptor(glue: Glue) {
): Unit = {

// If the registry is not consistent, this indicates a mistake in the users definition and we want to let him know.
registry.checkConsistency().left.foreach {
registry.checkConsistency(scenarioScoped).left.foreach {
(ex: IncorrectHookDefinitionException) =>
throw ex
}

registry.stepDefinitions
.map(ScalaStepDefinition(_, scenarioScoped))
.foreach(glue.addStepDefinition)

// The presence of beforeAll/afterAll hooks with scenarioScoped is checked by checkConsistency above
if (!scenarioScoped) {
registry.beforeAllHooks
.map(ScalaStaticHookDefinition(_))
.foreach(glue.addBeforeAllHook)
registry.afterAllHooks
.map(ScalaStaticHookDefinition(_))
.foreach(glue.addAfterAllHook)
}

registry.beforeHooks
.map(ScalaHookDefinition(_, scenarioScoped))
.foreach(glue.addBeforeHook)
Expand All @@ -35,6 +46,7 @@ class GlueAdaptor(glue: Glue) {
registry.afterStepHooks
.map(ScalaHookDefinition(_, scenarioScoped))
.foreach(glue.addAfterStepHook)

registry.docStringTypes
.map(ScalaDocStringTypeDefinition(_, scenarioScoped))
.foreach(glue.addDocStringType)
Expand All @@ -44,6 +56,7 @@ class GlueAdaptor(glue: Glue) {
registry.parameterTypes
.map(ScalaParameterTypeDefinition(_, scenarioScoped))
.foreach(glue.addParameterType)

registry.defaultParameterTransformers
.map(ScalaDefaultParameterTransformerDefinition(_, scenarioScoped))
.foreach(glue.addDefaultParameterTransformer)
Expand Down
69 changes: 62 additions & 7 deletions cucumber-scala/src/main/scala/io/cucumber/scala/HookDsl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ package io.cucumber.scala
private[scala] trait HookDsl extends BaseScalaDsl {
self =>

/** Defines a before all hook.
*/
def BeforeAll: StaticHookBody = BeforeAll(DEFAULT_AFTER_ORDER)

/** Defines a before all hook.
* @param order the order in which this hook should run. Higher numbers are run first
*/
def BeforeAll(order: Int): StaticHookBody = new StaticHookBody(
StaticHookType.BEFORE_ALL,
order,
Utils.frame(self)
)

/** Defines an before hook.
*/
def Before: HookBody = Before(EMPTY_TAG_EXPRESSION, DEFAULT_BEFORE_ORDER)
Expand All @@ -26,7 +39,12 @@ private[scala] trait HookDsl extends BaseScalaDsl {
* @param order the order in which this hook should run. Higher numbers are run first
*/
def Before(tagExpression: String, order: Int) =
new HookBody(HookType.BEFORE, tagExpression, order, Utils.frame(self))
new HookBody(
ScopedHookType.BEFORE,
tagExpression,
order,
Utils.frame(self)
)

/** Defines an before step hook.
*/
Expand All @@ -52,7 +70,22 @@ private[scala] trait HookDsl extends BaseScalaDsl {
* @param order the order in which this hook should run. Higher numbers are run first
*/
def BeforeStep(tagExpression: String, order: Int) =
new HookBody(HookType.BEFORE_STEP, tagExpression, order, Utils.frame(self))
new HookBody(
ScopedHookType.BEFORE_STEP,
tagExpression,
order,
Utils.frame(self)
)

/** Defines a after all hook.
*/
def AfterAll: StaticHookBody = AfterAll(DEFAULT_AFTER_ORDER)

/** Defines a after all hook.
* @param order the order in which this hook should run. Higher numbers are run first
*/
def AfterAll(order: Int): StaticHookBody =
new StaticHookBody(StaticHookType.AFTER_ALL, order, Utils.frame(self))

/** Defines and after hook.
*/
Expand All @@ -77,7 +110,7 @@ private[scala] trait HookDsl extends BaseScalaDsl {
* @param order the order in which this hook should run. Higher numbers are run first
*/
def After(tagExpression: String, order: Int) =
new HookBody(HookType.AFTER, tagExpression, order, Utils.frame(self))
new HookBody(ScopedHookType.AFTER, tagExpression, order, Utils.frame(self))

/** Defines and after step hook.
*/
Expand All @@ -102,10 +135,15 @@ private[scala] trait HookDsl extends BaseScalaDsl {
* @param order the order in which this hook should run. Higher numbers are run first
*/
def AfterStep(tagExpression: String, order: Int) =
new HookBody(HookType.AFTER_STEP, tagExpression, order, Utils.frame(self))
new HookBody(
ScopedHookType.AFTER_STEP,
tagExpression,
order,
Utils.frame(self)
)

final class HookBody(
hookType: HookType,
hookType: ScopedHookType,
tagExpression: String,
order: Int,
frame: StackTraceElement
Expand All @@ -120,8 +158,25 @@ private[scala] trait HookDsl extends BaseScalaDsl {
}

def apply(body: Scenario => Unit): Unit = {
val details = ScalaHookDetails(tagExpression, order, body)
registry.registerHook(hookType, details, frame)
val details = ScalaHookDetails(tagExpression, order, body, frame)
registry.registerDynamicHook(hookType, details)
}

}

final class StaticHookBody(
hookType: StaticHookType,
order: Int,
frame: StackTraceElement
) {

// When a HookBody is created, we want to ensure that the apply method is called
// To be able to check this, we notice the registry to expect a hook
registry.expectHook(hookType, frame)

def apply(body: => Unit): Unit = {
val details = ScalaStaticHookDetails(order, () => body, frame)
registry.registerStaticHook(hookType, details)
}

}
Expand Down
22 changes: 17 additions & 5 deletions cucumber-scala/src/main/scala/io/cucumber/scala/HookType.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@ package io.cucumber.scala

sealed trait HookType

object HookType {
sealed trait ScopedHookType extends HookType

case object BEFORE extends HookType
object ScopedHookType {

case object BEFORE_STEP extends HookType
case object BEFORE extends ScopedHookType

case object AFTER extends HookType
case object BEFORE_STEP extends ScopedHookType

case object AFTER_STEP extends HookType
case object AFTER extends ScopedHookType

case object AFTER_STEP extends ScopedHookType

}

sealed trait StaticHookType extends HookType

object StaticHookType {

case object BEFORE_ALL extends StaticHookType

case object AFTER_ALL extends StaticHookType

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package io.cucumber.scala

import io.cucumber.core.backend.CucumberBackendException

sealed abstract class IncorrectHookDefinitionException(message: String)
extends CucumberBackendException(message)

object IncorrectHookDefinitionException {

def errorMessage(expectedHooks: Seq[UndefinedHook]): String = {
def undefinedHooksErrorMessage(expectedHooks: Seq[UndefinedHook]): String = {
val hooksListToDisplay = expectedHooks.map { eh =>
s" - ${eh.stackTraceElement.getFileName}:${eh.stackTraceElement.getLineNumber} (${eh.hookType})"
}
Expand All @@ -29,11 +32,36 @@ object IncorrectHookDefinitionException {
|""".stripMargin
}

def scenarioScopedStaticHookErrorMessage(
staticHooks: Seq[ScalaStaticHookDetails]
): String = {
val hooksListToDisplay: Seq[String] = staticHooks.map { h =>
s" - ${h.stackTraceElement.getFileName}:${h.stackTraceElement.getLineNumber}"
}

s"""Some hooks are not defined properly:
|${hooksListToDisplay.mkString("\n")}
|
|This can be caused by defining static hooks (BeforeAll/AfterAll) in a class rather than in a object.
|Such hooks can only be defined in a static context.
|""".stripMargin
}

}

class IncorrectHookDefinitionException(val undefinedHooks: Seq[UndefinedHook])
extends CucumberBackendException(
IncorrectHookDefinitionException.errorMessage(undefinedHooks)
class UndefinedHooksException(val undefinedHooks: Seq[UndefinedHook])
extends IncorrectHookDefinitionException(
IncorrectHookDefinitionException.undefinedHooksErrorMessage(
undefinedHooks
)
) {}

class ScenarioScopedStaticHookException(
val staticHooks: Seq[ScalaStaticHookDetails]
) extends IncorrectHookDefinitionException(
IncorrectHookDefinitionException.scenarioScopedStaticHookErrorMessage(
staticHooks
)
) {}

case class UndefinedHook(
Expand Down
Loading