Skip to content
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

New resource step with explicit scopes #785

Merged
merged 1 commit into from
Jul 5, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ case class RunState(
// Only the session is propagated downstream as it is.
lazy val nestedContext: RunState = copy(depth = depth + 1, logStack = Nil, cleanupSteps = Nil)
lazy val sameLevelContext: RunState = copy(logStack = Nil, cleanupSteps = Nil)
lazy val upperLevelContext: RunState = copy(depth = depth - 1, logStack = Nil, cleanupSteps = Nil)

def withSession(s: Session): RunState = copy(session = s)
def addToSession(tuples: Seq[(String, String)]): RunState = withSession(session.addValuesUnsafe(tuples: _*))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,5 @@ trait WrapperStep extends Step {
def successTitleLog(depth: Int) = SuccessLogInstruction(title, depth)
}

// Describes the lifecycle of a resource
case class Resource(title: String, acquire: Step, release: Step)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import cats.Show
import cats.syntax.either._
import cats.syntax.show._
import cats.effect.IO
import com.github.agourlay.cornichon.core.{ CornichonError, FeatureDef, ScenarioContext, Session, SessionKey, Step, Scenario => ScenarioDef }
import com.github.agourlay.cornichon.core.{ CornichonError, FeatureDef, Resource, ScenarioContext, Session, SessionKey, Step, Scenario => ScenarioDef }
import com.github.agourlay.cornichon.dsl.SessionSteps.{ SessionHistoryStepBuilder, SessionStepBuilder, SessionValuesStepBuilder }
import com.github.agourlay.cornichon.steps.cats.EffectStep
import com.github.agourlay.cornichon.steps.regular.DebugStep
Expand Down Expand Up @@ -125,6 +125,11 @@ trait CoreDsl {
WithDataInputStep(steps, where, rawJson = true)
}

def WithResource(resource: Resource): BodyElementCollector[Step, Step] =
BodyElementCollector[Step, Step] { steps =>
WithResourceStep(steps, resource)
}

def wait(duration: FiniteDuration): Step = EffectStep.fromAsync(
title = s"wait for ${duration.toMillis} millis",
effect = sc => IO.delay(sc.session).delayBy(duration)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.github.agourlay.cornichon.steps.wrapped

import cats.data.StateT
import cats.effect.IO
import com.github.agourlay.cornichon.core.Done.rightDone
import com.github.agourlay.cornichon.core.{ FailureLogInstruction, Resource, ScenarioRunner, Step, StepState, SuccessLogInstruction, WrapperStep }

// The `Resource` is created before `nested` and released after
case class WithResourceStep(nested: List[Step], resource: Resource) extends WrapperStep {
val title = s"With resource block `${resource.title}`"

override val stateUpdate: StepState = StateT { runState =>
val initialDepth = runState.depth
val resourceNested = runState.nestedContext
ScenarioRunner.runStepsShortCircuiting(resource.acquire :: Nil, resourceNested).flatMap { resTuple =>
val (acquireState, acquireRes) = resTuple
acquireRes match {
case Left(_) =>
val logs = FailureLogInstruction("With resource block failed due to acquire step", initialDepth) +: acquireState.logStack :+ failedTitleLog(initialDepth)
IO.pure((runState.mergeNested(acquireState, logs), acquireRes))
case Right(_) =>
val nestedNested = acquireState.nestedContext
ScenarioRunner.runStepsShortCircuiting(nested, nestedNested).flatMap { resTuple =>
val (nestedState, nestedRes) = resTuple
// always trigger resource release
ScenarioRunner.runStepsShortCircuiting(resource.release :: Nil, nestedState.upperLevelContext).flatMap { resTuple =>
val (releaseState, releaseRes) = resTuple
// gather all logs
val innerLogs = releaseState.logStack ::: nestedState.logStack ::: acquireState.logStack
val (report, logs) = (nestedRes, releaseRes) match {
case (Left(_), _) =>
(nestedRes, FailureLogInstruction("With resource block failed", initialDepth) +: innerLogs :+ failedTitleLog(initialDepth))
case (_, Left(_)) =>
(releaseRes, FailureLogInstruction("With resource block failed due to release step", initialDepth) +: innerLogs :+ failedTitleLog(initialDepth))
case _ =>
(rightDone, SuccessLogInstruction("With resource block succeeded", initialDepth) +: innerLogs :+ successTitleLog(initialDepth))
}
// propagate potential cleanup steps
val mergedState = runState.recordLogStack(logs)
.registerCleanupSteps(nestedState.cleanupSteps)
.registerCleanupSteps(acquireState.cleanupSteps)
.registerCleanupSteps(releaseState.cleanupSteps)
IO.pure((mergedState, report))
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.github.agourlay.cornichon.steps.wrapped

import com.github.agourlay.cornichon.testHelpers.CommonTestSuite
import com.github.agourlay.cornichon.core.{ Resource, Scenario, ScenarioRunner, Session }
import com.github.agourlay.cornichon.steps.regular.assertStep.{ AssertStep, Assertion }
import munit.FunSuite

class WithResourceStepSpec extends FunSuite with CommonTestSuite {

test("succeed if acquire + nested + release are valid") {
val resource = Resource(
"super neat resource",
acquire = AssertStep("Acquire step", _ => Assertion.alwaysValid),
release = AssertStep("Release step", _ => Assertion.alwaysValid))

val nested = AssertStep("Nested step", _ => Assertion.alwaysValid) :: Nil
val withResourceStep = WithResourceStep(nested, resource)
val s = Scenario("scenario with resource that fails", withResourceStep :: Nil)
val res = awaitIO(ScenarioRunner.runScenario(Session.newEmpty)(s))
assert(res.isSuccess)

matchLogsWithoutDuration(res.logs) {
"""
| Scenario : scenario with resource that fails
| main steps
| With resource block `super neat resource`
| Acquire step
| Nested step
| Release step
| With resource block succeeded""".stripMargin
}
}

test("fails if acquire fails") {
val resource = Resource(
"super neat resource",
acquire = AssertStep("Acquire step", _ => Assertion.failWith("Something went wrong")),
release = AssertStep("Release step", _ => Assertion.alwaysValid))

val nested = AssertStep("Nested step", _ => Assertion.alwaysValid) :: Nil
val withResourceStep = WithResourceStep(nested, resource)
val s = Scenario("scenario with resource that fails", withResourceStep :: Nil)
val res = awaitIO(ScenarioRunner.runScenario(Session.newEmpty)(s))
scenarioFailsWithMessage(res) {
"""Scenario 'scenario with resource that fails' failed:
|
|at step:
|Acquire step
|
|with error(s):
|Something went wrong
|
|seed for the run was '1'
|""".stripMargin
}

matchLogsWithoutDuration(res.logs) {
"""
| Scenario : scenario with resource that fails
| main steps
| With resource block `super neat resource`
| Acquire step
| *** FAILED ***
| Something went wrong
| With resource block failed due to acquire step""".stripMargin
}
}

test("fails if nested fails") {
val resource = Resource(
"super neat resource",
acquire = AssertStep("Acquire step", _ => Assertion.alwaysValid),
release = AssertStep("Release step", _ => Assertion.alwaysValid))

val nested = AssertStep("Nested step", _ => Assertion.failWith("Something went wrong")) :: Nil
val withResourceStep = WithResourceStep(nested, resource)
val s = Scenario("scenario with resource that fails", withResourceStep :: Nil)
val res = awaitIO(ScenarioRunner.runScenario(Session.newEmpty)(s))
scenarioFailsWithMessage(res) {
"""Scenario 'scenario with resource that fails' failed:
|
|at step:
|Nested step
|
|with error(s):
|Something went wrong
|
|seed for the run was '1'
|""".stripMargin
}

matchLogsWithoutDuration(res.logs) {
"""
| Scenario : scenario with resource that fails
| main steps
| With resource block `super neat resource`
| Acquire step
| Nested step
| *** FAILED ***
| Something went wrong
| Release step
| With resource block failed""".stripMargin
}
}

test("fails if release fails") {
val resource = Resource(
"super neat resource",
acquire = AssertStep("Acquire step", _ => Assertion.alwaysValid),
release = AssertStep("Release step", _ => Assertion.failWith("Something went wrong")))

val nested = AssertStep("Nested step", _ => Assertion.alwaysValid) :: Nil
val withResourceStep = WithResourceStep(nested, resource)
val s = Scenario("scenario with resource that fails", withResourceStep :: Nil)
val res = awaitIO(ScenarioRunner.runScenario(Session.newEmpty)(s))
scenarioFailsWithMessage(res) {
"""Scenario 'scenario with resource that fails' failed:
|
|at step:
|Release step
|
|with error(s):
|Something went wrong
|
|seed for the run was '1'
|""".stripMargin
}

matchLogsWithoutDuration(res.logs) {
"""
| Scenario : scenario with resource that fails
| main steps
| With resource block `super neat resource`
| Acquire step
| Nested step
| Release step
| *** FAILED ***
| Something went wrong
| With resource block failed due to release step""".stripMargin
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ package com.github.agourlay.cornichon.framework.examples.superHeroes

import java.nio.charset.StandardCharsets
import java.util.Base64

import com.github.agourlay.cornichon.CornichonFeature
import com.github.agourlay.cornichon.core.Step
import com.github.agourlay.cornichon.core.Resource
import com.github.agourlay.cornichon.framework.examples.HttpServer
import com.github.agourlay.cornichon.framework.examples.superHeroes.server.SuperHeroesHttpAPI
import com.github.agourlay.cornichon.http.HttpService
import com.github.agourlay.cornichon.json.CornichonJson._
import com.github.agourlay.cornichon.resolver.JsonMapper
import com.github.agourlay.cornichon.steps.wrapped.ScenarioResourceStep
import sangria.macros._

import scala.concurrent.Await
import scala.concurrent.duration._

Expand Down Expand Up @@ -759,6 +758,21 @@ class SuperHeroesScenario extends CornichonFeature {
"""
)
}

Scenario("demonstrate resource steps") {
When I get("/superheroes/Flash").withParams("sessionId" -> "<session-id>")
Then assert status.is(404)

WithSuperhero("Flash") {

When I get("/superheroes/Flash").withParams("sessionId" -> "<session-id>")

Then assert status.is(200)
}

When I get("/superheroes/Flash").withParams("sessionId" -> "<session-id>")
Then assert status.is(404)
}
}

def superhero_exists(name: String): Step =
Expand All @@ -773,6 +787,31 @@ class SuperHeroesScenario extends CornichonFeature {
Then assert body.path("name").is(name)
}

// Create a superhero resource that will be cleaned up at the end of the scope
def WithSuperhero(name: String) = WithResource(superhero_resource(name))

def superhero_resource(name: String) = Resource(
title = s"superhero $name resource",
acquire = post("/superheroes")
.withBody(
s"""
{
"name": "$name",
"realName": "unknown",
"city": "Berlin",
"hasSuperpowers": true,
"publisher": {
"name":"DC",
"foundationYear":1934,
"location":"Burbank, California"
}
}
""")
.withParams("sessionId" -> "<session-id>")
.withHeaders(("Authorization", "Basic " + Base64.getEncoder.encodeToString("admin:cornichon".getBytes(StandardCharsets.UTF_8)))),
release = delete(s"/superheroes/<name>").withParams("sessionId" -> "<session-id>")
)

lazy val port = 8080

// Base url used for all HTTP steps
Expand Down