diff --git a/README.md b/README.md index d2916dd..5852e69 100644 --- a/README.md +++ b/README.md @@ -32,42 +32,18 @@ The NFA is used to generate test cases (sequence of UI actions) with the help of For example, whenever a random action is executed with the help of monkey testing, it adds a transition to the state machine. After running the genetic algorithm, the state machine is then used to create a test suite. -**At the moment, the following definitions are incomplete and must be adapted to the actual implementation which calls this service.** - -### Test Suite -A set of test cases. - -### Test Case -A sequence of UI actions. - -### UI Action -An action which can be triggered by the user via the GUI. - -### UI Path -A sequence of states with transitions from one state to another. -Each transition is a UI action. - -### State -A state is defined by the set of all visible and interactable windows together with their enabled widgets. - ## Scala API for GUI State Machines The package [api](./src/main/scala/de/retest/guistatemachine/api/) contains all types and methods for getting and modifying the GUI state machine. ## REST API At the moment there is only an initial version of a REST API which has to be mapped to the Scala API. -Some suggestions how the REST API for the state machine could look like: -* `/state-machines` GET queries all existing state machines. -* `/create-state-machine` POST creates a new state machine. -* `/state-machine/` GET queries an existing state machine. -* `/state-machine//states` GET queries all existing states of the state machine. -* `/state-machine//state/` GET queries a specific state of the state machine which contains transitions. -* `/state-machine//state//transitions` GET queries all transitions of a specific state. -* `/state-machine//state//transition/` GET queries a specific transition of a specific state. -* `/state-machine//execute` POST executes the passed action from the passed state which might lead to a new state and adds a transition to the state machine. The action must be part of all actions? - -## Swagger Support +The REST service can be started with `sbt run`. +It has the address `http://localhost:8888/`. +The REST API can be tested manually with `curl`: +```bash +curl -H "Content-Type: application/json" -X POST http://localhost:8888/state-machine +``` + +### Swagger Support The Swagger support is based on [swagger-akka-http](https://github.com/swagger-akka-http/swagger-akka-http). -The URL `http://localhost:8888/api-docs/swagger.json` should show create Swagger JSON output which can be rendered by Swagger UI. - -### Bash Scripts for REST Calls -The directory [scripts](./scripts) contains a number of Bash scripts which use `curl` to send REST calls to a running server. \ No newline at end of file +The URL `http://localhost:8888/api-docs/swagger.json` should show create Swagger JSON output which can be rendered by Swagger UI. \ No newline at end of file diff --git a/scripts/application.sh b/scripts/application.sh deleted file mode 100644 index 21d8fa2..0000000 --- a/scripts/application.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -H "Content-Type: application/json" -X GET http://localhost:8888/application/0 \ No newline at end of file diff --git a/scripts/applications.sh b/scripts/applications.sh deleted file mode 100644 index a61b8e8..0000000 --- a/scripts/applications.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -H "Content-Type: application/json" -X GET http://localhost:8888/applications \ No newline at end of file diff --git a/scripts/create-application.sh b/scripts/create-application.sh deleted file mode 100644 index ad08926..0000000 --- a/scripts/create-application.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -H "Content-Type: application/json" -X POST http://localhost:8888/create-application \ No newline at end of file diff --git a/scripts/create-test-suite.sh b/scripts/create-test-suite.sh deleted file mode 100644 index cb967d2..0000000 --- a/scripts/create-test-suite.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -H "Content-Type: application/json" -X POST http://localhost:8888/application/0/create-test-suite \ No newline at end of file diff --git a/scripts/delete-application.sh b/scripts/delete-application.sh deleted file mode 100644 index f66cf9e..0000000 --- a/scripts/delete-application.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -H "Content-Type: application/json" -X DELETE http://localhost:8888/application/0 \ No newline at end of file diff --git a/scripts/delete-test-suite.sh b/scripts/delete-test-suite.sh deleted file mode 100644 index 0648c0f..0000000 --- a/scripts/delete-test-suite.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -H "Content-Type: application/json" -X DELETE http://localhost:8888/application/0/test-suite/0 \ No newline at end of file diff --git a/scripts/runcompleterestservicetest.sh b/scripts/runcompleterestservicetest.sh deleted file mode 100644 index 2f796d1..0000000 --- a/scripts/runcompleterestservicetest.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/bin/bash -echo "List applications, should be empty:" -bash applications.sh -echo "" -echo "Request application, should not be found:" -bash application.sh -echo "" -echo "Request test suites, should not be found:" -bash test-suites.sh -echo "" -echo "Request test suite, should not be found:" -bash test-suite.sh - -echo "" -echo "Create application, should work:" -bash create-application.sh -echo "" -echo "List applications, should contain one element:" -bash applications.sh -echo "" -echo "Request application, should be found and contain test suites:" -bash application.sh -echo "" -echo "Request test suites, should be found and empty:" -bash test-suites.sh -echo "" -echo "Request test suite, should not be found:" -bash test-suite.sh - -echo "" -echo "Create test suite, should work:" -bash create-test-suite.sh -echo "" -echo "Request test suites, should be found and contain one test suite:" -bash test-suites.sh -echo "" -echo "Request test suite, should be found:" -bash test-suite.sh - -echo "" -echo "Delete test suite, should work:" -bash delete-test-suite.sh -echo "" -echo "Request test suite, should not be found:" -bash test-suite.sh - -echo "" -echo "Delete application suite, should work:" -bash delete-application.sh -echo "" -echo "Request test suite, should not be found:" -bash application.sh \ No newline at end of file diff --git a/scripts/test-suite.sh b/scripts/test-suite.sh deleted file mode 100644 index 3fa928c..0000000 --- a/scripts/test-suite.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -H "Content-Type: application/json" -X GET http://localhost:8888/application/0/test-suite/0 \ No newline at end of file diff --git a/scripts/test-suites.sh b/scripts/test-suites.sh deleted file mode 100644 index 0c45a72..0000000 --- a/scripts/test-suites.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -curl -H "Content-Type: application/json" -X GET http://localhost:8888/application/0/test-suites \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala index f3ed50e..4776573 100644 --- a/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala +++ b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala @@ -7,15 +7,23 @@ trait GuiStateMachineApi { * * @return The new GUI state machine. */ - def createStateMachine: GuiStateMachine + def createStateMachine(): Id /** * Removes an existing [[GuiStateMachine]]. * - * @param stateMachine The persisted GUI state machine. + * @param id The ID of the GUI state machine. * @return True if it existed and was removed by this call. Otherwise, false. */ - def removeStateMachine(stateMachine: GuiStateMachine): Boolean + def removeStateMachine(id: Id): Boolean + + /** + * Gets an existing [[GuiStateMachine]]. + * + * @param id The ID of the GUI state machine. + * @return The existing GUI state machine or nothing. + */ + def getStateMachine(id: Id): Option[GuiStateMachine] /** * Stores all state machines on the disk. diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Id.scala b/src/main/scala/de/retest/guistatemachine/api/Id.scala similarity index 73% rename from src/main/scala/de/retest/guistatemachine/rest/model/Id.scala rename to src/main/scala/de/retest/guistatemachine/api/Id.scala index f3ea467..fcbc866 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Id.scala +++ b/src/main/scala/de/retest/guistatemachine/api/Id.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine.rest.model +package de.retest.guistatemachine.api final case class Id(val id: Long) extends Ordered[Id] { diff --git a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImpl.scala b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImpl.scala index 17db53a..363c563 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImpl.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImpl.scala @@ -1,20 +1,17 @@ package de.retest.guistatemachine.api.impl -import de.retest.guistatemachine.api.GuiStateMachineApi -import de.retest.guistatemachine.api.GuiStateMachine +import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi, Id} -import scala.collection.mutable.HashSet +import scala.collection.immutable.HashMap object GuiStateMachineApiImpl extends GuiStateMachineApi { - val stateMachines = new HashSet[GuiStateMachine] + val stateMachines = IdMap(new HashMap[Id, GuiStateMachine]) - override def createStateMachine: GuiStateMachine = { - val r = new GuiStateMachineImpl - stateMachines += r - r - } + override def createStateMachine(): Id = stateMachines.addNewElement(new GuiStateMachineImpl) + + override def removeStateMachine(id: Id): Boolean = stateMachines.removeElement(id) - override def removeStateMachine(stateMachine: GuiStateMachine): Boolean = stateMachines.remove(stateMachine) + override def getStateMachine(id: Id): Option[GuiStateMachine] = stateMachines.getElement(id) override def persist(): Unit = { // TODO #9 store on the disk diff --git a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala index 9e384e0..68920a1 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala @@ -2,7 +2,6 @@ package de.retest.guistatemachine.api.impl import de.retest.guistatemachine.api.{Action, Descriptors, GuiStateMachine, State} import scala.collection.immutable.{HashMap, HashSet} - class GuiStateMachineImpl extends GuiStateMachine { var states = new HashMap[Descriptors, State] diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Map.scala b/src/main/scala/de/retest/guistatemachine/api/impl/IdMap.scala similarity index 68% rename from src/main/scala/de/retest/guistatemachine/rest/model/Map.scala rename to src/main/scala/de/retest/guistatemachine/api/impl/IdMap.scala index 599bae8..5e62db7 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Map.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/IdMap.scala @@ -1,13 +1,14 @@ -package de.retest.guistatemachine.rest.model +package de.retest.guistatemachine.api.impl + +import de.retest.guistatemachine.api.Id import scala.collection.immutable.HashMap /** * This custom type allows storing values using [[Id]] as key. - * [[de.retest.guistatemachine.rest.JsonFormatForIdMap]] implements marshalling and unmarshalling for JSON for this type. * We cannot extend immutable maps in Scala, so we have to keep it as field. */ -case class Map[T](var values: scala.collection.immutable.Map[Id, T] = new HashMap[Id, T]) { +case class IdMap[T](var values: scala.collection.immutable.Map[Id, T] = new HashMap[Id, T]) { /** * Generates a new ID based on the existing entries. @@ -28,14 +29,14 @@ case class Map[T](var values: scala.collection.immutable.Map[Id, T] = new HashMa false } - def getElement(id: Id): T = values(id) + def getElement(id: Id): Option[T] = values.get(id) def hasElement(id: Id): Boolean = values.contains(id) } -object Map { - def fromValues[T](v: T*): Map[T] = { - val r = Map[T]() +object IdMap { + def fromValues[T](v: T*): IdMap[T] = { + val r = IdMap[T]() for (e <- v) r.addNewElement(e) r } diff --git a/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala b/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala deleted file mode 100644 index 18275a9..0000000 --- a/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala +++ /dev/null @@ -1,19 +0,0 @@ -package de.retest.guistatemachine.persistence - -import de.retest.guistatemachine.rest.model.{Id, Map, StateMachine, StateMachines} - -import scala.collection.immutable.HashMap - -class Persistence { - // database - private val stateMachines = StateMachines(Map(new HashMap[Id, StateMachine])) - - def getStateMachines(): StateMachines = stateMachines - - def getStateMachine(id: Id): Option[StateMachine] = if (stateMachines.stateMachines.hasElement(id)) Some(stateMachines.stateMachines.getElement(id)) else None - - // TODO #1 Pass all unexplored actions for the initial state! - def createStateMachine(): Id = stateMachines.stateMachines.addNewElement(StateMachine()) - - def deleteStateMachine(id: Id): Boolean = stateMachines.stateMachines.removeElement(id) -} diff --git a/src/main/scala/de/retest/guistatemachine/rest/DefaultJsonFormats.scala b/src/main/scala/de/retest/guistatemachine/rest/DefaultJsonFormats.scala deleted file mode 100644 index 46cbc7a..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/DefaultJsonFormats.scala +++ /dev/null @@ -1,21 +0,0 @@ -package de.retest.guistatemachine.rest - -import de.retest.guistatemachine.rest.model.{Action, Actions, Id, State, StateMachine, StateMachines, States, Transition, Transitions} -import spray.json.DefaultJsonProtocol._ - -trait DefaultJsonFormats { - // formats for unmarshalling and marshalling - implicit val idFormat = jsonFormat1(Id) - implicit val actionFormat = jsonFormat0(Action) - implicit val idMapFormatActions = new JsonFormatForIdMap[Action] - implicit val actionsFormat = jsonFormat1(Actions) - implicit val transitionFormat = jsonFormat2(Transition) - implicit val idMapFormatTransitions = new JsonFormatForIdMap[Transition] - implicit val transitionsFormat = jsonFormat1(Transitions) - implicit val stateFormat = jsonFormat1(State) - implicit val idMapFormatState = new JsonFormatForIdMap[State] - implicit val statesFormat = jsonFormat1(States) - implicit val stateMachineFormat = jsonFormat2(StateMachine) - implicit val idMapFormatStateMachines = new JsonFormatForIdMap[StateMachine] - implicit val stateMachinesFormat = jsonFormat1(StateMachines) -} diff --git a/src/main/scala/de/retest/guistatemachine/rest/ExecuteActionBody.scala b/src/main/scala/de/retest/guistatemachine/rest/ExecuteActionBody.scala new file mode 100644 index 0000000..c136948 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/ExecuteActionBody.scala @@ -0,0 +1,5 @@ +package de.retest.guistatemachine.rest + +import de.retest.guistatemachine.api.{Action, Descriptors, State} + +case class ExecuteActionBody(from: State, a: Action, descriptors: Descriptors, neverExploredActions: Set[Action]) diff --git a/src/main/scala/de/retest/guistatemachine/rest/GetStateBody.scala b/src/main/scala/de/retest/guistatemachine/rest/GetStateBody.scala new file mode 100644 index 0000000..4e9c2f6 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/GetStateBody.scala @@ -0,0 +1,5 @@ +package de.retest.guistatemachine.rest + +import de.retest.guistatemachine.api.{Action, Descriptors} + +case class GetStateBody(descriptors: Descriptors, neverExploredActions: Set[Action]) diff --git a/src/main/scala/de/retest/guistatemachine/rest/GuiStateMachineService.scala b/src/main/scala/de/retest/guistatemachine/rest/GuiStateMachineService.scala new file mode 100644 index 0000000..4d985f4 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/GuiStateMachineService.scala @@ -0,0 +1,83 @@ +package de.retest.guistatemachine.rest + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import akka.http.scaladsl.model.{StatusCode, StatusCodes} +import akka.http.scaladsl.server.{Directives, Route} +import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi, Id, State} +import de.retest.guistatemachine.rest.json.DefaultJsonFormats +import io.swagger.annotations.{Api, ApiOperation, ApiResponse, ApiResponses} +import javax.ws.rs.Path + +@Api(value = "/state-machine", description = "Gets a state machine") +@Path("/state-machine") +class GuiStateMachineService(api: GuiStateMachineApi) extends Directives with DefaultJsonFormats { + + def getRoute(): Route = getStateMachine() ~ deleteStateMachine() ~ postStateMachine() ~ getState() + + @ApiOperation(httpMethod = "GET", response = classOf[GuiStateMachine], value = "Returns a state machine based on the ID") + @ApiResponses(Array(new ApiResponse(code = 404, message = "State machine not found"))) + def getStateMachine(): Route = get { + path("state-machine" / LongNumber) { id => + val r = api.getStateMachine(Id(id)) + r match { + case Some(x) => complete(x) + case None => complete(StatusCodes.NotFound) + } + } + } + + @ApiOperation(httpMethod = "DELETE", response = classOf[StatusCode], value = "Returns the status code") + @ApiResponses( + Array( + new ApiResponse(code = 200, message = "Successful deletion"), + new ApiResponse(code = 404, message = "State machine not found") + )) + def deleteStateMachine(): Route = delete { + path("state-machine" / LongNumber) { stateMachineId => + import de.retest.guistatemachine.api.Id + val r = api.removeStateMachine(Id(stateMachineId)) + complete(if (r) StatusCodes.OK else StatusCodes.NotFound) + } + } + + @ApiOperation(httpMethod = "POST", response = classOf[Id], value = "Returns the ID") + @ApiResponses(Array(new ApiResponse(code = 200, message = "Successful creation"))) + def postStateMachine(): Route = post { + path("state-machine") { + val id = api.createStateMachine() + complete(id) + } + } + + @ApiOperation(httpMethod = "POST", response = classOf[State], value = "Returns the existing or newly created state") + @ApiResponses(Array(new ApiResponse(code = 404, message = "State machine not found"))) + def getState(): Route = post { + path("state-machine" / LongNumber / "get-state") { id => + val app = api.getStateMachine(Id(id)) + app match { + case Some(x) => { + entity(as[GetStateBody]) { body => + complete(x.getState(body.descriptors, body.neverExploredActions)) + } + } + case None => complete(StatusCodes.NotFound) + } + } + } + + @ApiOperation(httpMethod = "POST", response = classOf[State], value = "Returns the state which is reached by executing this action") + @ApiResponses(Array(new ApiResponse(code = 404, message = "State machine not found"))) + def executeAction(): Route = post { + path("state-machine" / LongNumber / "execute-action") { id => + val app = api.getStateMachine(Id(id)) + app match { + case Some(x) => { + entity(as[ExecuteActionBody]) { body => + complete(x.executeAction(body.from, body.a, body.descriptors, body.neverExploredActions)) + } + } + case None => complete(StatusCodes.NotFound) + } + } + } +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala b/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala deleted file mode 100644 index 91eef8c..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala +++ /dev/null @@ -1,27 +0,0 @@ -package de.retest.guistatemachine.rest - -import de.retest.guistatemachine.rest.model.Id -import spray.json.{JsValue, JsonFormat, RootJsonFormat} - -/** - * Transforms a [[model.Map]] into a `scala.collection.immutable.Map[String, T]`, so it can be converted into valid JSON. - * Besides, transforms a JSON object which is a `scala.collection.immutable.Map[String, T]` back into a [[model.Map]]. - * This transformer requires a JSON format for the type `K`. - */ -class JsonFormatForIdMap[T](implicit - val jsonFormat0: JsonFormat[scala.collection.immutable.Map[String, T]], - implicit val jsonFormat1: JsonFormat[T]) - extends RootJsonFormat[model.Map[T]] { - - override def write(obj: model.Map[T]): JsValue = - jsonFormat0.write(obj.values.map { field => - (field._1.id.toString -> field._2) - }) - - override def read(json: JsValue): model.Map[T] = { - val map = jsonFormat0.read(json) - new model.Map[T](map.map { x => - (Id(x._1.toLong) -> x._2) - }) - } -} diff --git a/src/main/scala/de/retest/guistatemachine/rest/RestService.scala b/src/main/scala/de/retest/guistatemachine/rest/RestService.scala index 55544e4..0cbc3d1 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/RestService.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/RestService.scala @@ -2,18 +2,17 @@ package de.retest.guistatemachine.rest import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import de.retest.guistatemachine.persistence.Persistence +import de.retest.guistatemachine.api.GuiStateMachineApi trait RestService { - def getRoute(persistence: Persistence): Route = { - val stateMachinesService = new StateMachinesService(persistence) - val stateMachineService = new StateMachineService(persistence) + + def getRoute(guiStateMachineApi: GuiStateMachineApi): Route = { + val guiStateMachineService = new GuiStateMachineService(guiStateMachineApi) get { pathSingleSlash { complete("GUI State Machine API") } - } ~ stateMachinesService.getRoute() ~ stateMachineService.getRoute() ~ getFromResourceDirectory("swagger") ~ SwaggerDocService.routes + } ~ guiStateMachineService.getRoute() ~ SwaggerDocService.routes } - // TODO #1 Add static Swagger UI files to ~ path("swagger") { getFromResource("swagger/index.html") } } diff --git a/src/main/scala/de/retest/guistatemachine/rest/StateMachineService.scala b/src/main/scala/de/retest/guistatemachine/rest/StateMachineService.scala deleted file mode 100644 index 4ecb5b0..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/StateMachineService.scala +++ /dev/null @@ -1,51 +0,0 @@ -package de.retest.guistatemachine.rest - -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.{Directives, Route} -import de.retest.guistatemachine.persistence.Persistence -import de.retest.guistatemachine.rest.model.{Id, StateMachine} -import io.swagger.annotations.{Api, ApiOperation, ApiResponse, ApiResponses} -import javax.ws.rs.Path -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ -import akka.http.scaladsl.server.Directives._ - -@Api(value = "/state-machine", description = "Gets a state machine") -@Path("/state-machine") -class StateMachineService(persistence: Persistence) extends Directives with DefaultJsonFormats { - - def getRoute(): Route = getStateMachine() ~ deleteStateMachine() ~ postStateMachine() - - @ApiOperation(httpMethod = "GET", response = classOf[StateMachine], value = "Returns a state machine based on the ID") - @ApiResponses(Array(new ApiResponse(code = 404, message = "State machine not found"))) - def getStateMachine(): Route = get { - path("state-machine" / LongNumber) { id => - val app = persistence.getStateMachine(Id(id)) - app match { - case Some(x) => complete(x) - case None => complete(StatusCodes.NotFound) - } - } - } - - @ApiOperation(httpMethod = "DELETE", response = classOf[akka.http.scaladsl.model.StatusCode], value = "Returns the status code") - @ApiResponses( - Array( - new ApiResponse(code = 200, message = "Successful deletion"), - new ApiResponse(code = 404, message = "State machine not found") - )) - def deleteStateMachine(): Route = delete { - path("state-machine" / LongNumber) { stateMachineId => - val r = persistence.deleteStateMachine(Id(stateMachineId)) - complete(if (r) StatusCodes.OK else StatusCodes.NotFound) - } - } - - @ApiOperation(httpMethod = "POST", response = classOf[Id], value = "Returns the ID") - @ApiResponses(Array(new ApiResponse(code = 200, message = "Successful creation"))) - def postStateMachine(): Route = post { - path("state-machine") { - val id = persistence.createStateMachine() - complete(id) - } - } -} diff --git a/src/main/scala/de/retest/guistatemachine/rest/StateMachinesService.scala b/src/main/scala/de/retest/guistatemachine/rest/StateMachinesService.scala deleted file mode 100644 index fd6c334..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/StateMachinesService.scala +++ /dev/null @@ -1,23 +0,0 @@ -package de.retest.guistatemachine.rest - -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ -import akka.http.scaladsl.server.{Directives, Route} -import de.retest.guistatemachine.persistence.Persistence -import de.retest.guistatemachine.rest.model.StateMachines -import io.swagger.annotations.{Api, ApiOperation, ApiResponses} -import javax.ws.rs.Path -import akka.http.scaladsl.server.Directives._ - -@Api(value = "/state-machines", description = "Operations about state machines") -@Path("/state-machines") -class StateMachinesService(persistence: Persistence) extends Directives with DefaultJsonFormats { - - def getRoute(): Route = getStateMachines() - - @ApiOperation(httpMethod = "GET", response = classOf[StateMachines], value = "Returns all state machines") - @ApiResponses(Array()) - def getStateMachines(): Route = - path("state-machines") { - complete(persistence.getStateMachines()) - } -} diff --git a/src/main/scala/de/retest/guistatemachine/rest/SwaggerDocService.scala b/src/main/scala/de/retest/guistatemachine/rest/SwaggerDocService.scala index bdae799..c2711c9 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/SwaggerDocService.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/SwaggerDocService.scala @@ -5,7 +5,7 @@ import com.github.swagger.akka.model.Info import io.swagger.models.ExternalDocs object SwaggerDocService extends SwaggerHttpService { - override val apiClasses = Set(classOf[RestService]) + override val apiClasses = Set(classOf[GuiStateMachineService]) override val host = s"${WebServer.Host}:${WebServer.Port}" override val info = Info(version = "1.0") override val externalDocs = Some(new ExternalDocs().description("Core Docs").url("http://acme.com/docs")) diff --git a/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala b/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala index 0d14f56..38b5d32 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala @@ -4,7 +4,7 @@ import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.server.RouteResult.route2HandlerFlow import akka.stream.ActorMaterializer -import de.retest.guistatemachine.persistence.Persistence +import de.retest.guistatemachine.api.impl.GuiStateMachineApiImpl import scopt.OptionParser import scala.io.StdIn @@ -32,9 +32,7 @@ object WebServer extends App with RestService { // parser.parse returns Option[C] parser.parse(args, Config()) match { case Some(config) => - val persistence = new Persistence - - val bindingFuture = Http().bindAndHandle(getRoute(persistence), Host, Port) + val bindingFuture = Http().bindAndHandle(getRoute(GuiStateMachineApiImpl), Host, Port) println(s"Server online at http://${Host}:${Port}/") diff --git a/src/main/scala/de/retest/guistatemachine/rest/json/DefaultJsonFormats.scala b/src/main/scala/de/retest/guistatemachine/rest/json/DefaultJsonFormats.scala new file mode 100644 index 0000000..b4778c0 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/json/DefaultJsonFormats.scala @@ -0,0 +1,14 @@ +package de.retest.guistatemachine.rest.json + +import de.retest.guistatemachine.api.Id + +import spray.json.DefaultJsonProtocol._ + +trait DefaultJsonFormats { + // formats for unmarshalling and marshalling + implicit val idFormat = jsonFormat1(Id) + implicit val getStateBodyFormat = new JsonFormatForGetStateBody + implicit val executeActionBodyFormat = new JsonFormatForExecuteActionBody + implicit val stateFormat = new JsonFormatForState + implicit val guiStateMachineFormat = new JsonFormatForGuiStateMachine +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForExecuteActionBody.scala b/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForExecuteActionBody.scala new file mode 100644 index 0000000..f64427b --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForExecuteActionBody.scala @@ -0,0 +1,14 @@ +package de.retest.guistatemachine.rest.json + +import de.retest.guistatemachine.rest.ExecuteActionBody +import spray.json.{JsObject, JsValue, RootJsonFormat} + +class JsonFormatForExecuteActionBody extends RootJsonFormat[ExecuteActionBody] { + import de.retest.guistatemachine.rest.ExecuteActionBody + + // TODO #1 Convert a ExecuteActionBody into JSON + override def write(obj: ExecuteActionBody): JsValue = JsObject() + + // TODO #1 Convert JSON into ExecuteActionBody + override def read(json: JsValue): ExecuteActionBody = throw new RuntimeException("Implementation is missing!") +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForGetStateBody.scala b/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForGetStateBody.scala new file mode 100644 index 0000000..e755969 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForGetStateBody.scala @@ -0,0 +1,14 @@ +package de.retest.guistatemachine.rest.json + +import de.retest.guistatemachine.rest.GetStateBody +import spray.json.{JsObject, JsValue, RootJsonFormat} + +class JsonFormatForGetStateBody extends RootJsonFormat[GetStateBody] { + import de.retest.guistatemachine.rest.GetStateBody + + // TODO #1 Convert a GetStateBody into JSON + override def write(obj: GetStateBody): JsValue = JsObject() + + // TODO #1 Convert JSON into GetStateBody + override def read(json: JsValue): GetStateBody = throw new RuntimeException("Implementation is missing!") +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForGuiStateMachine.scala b/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForGuiStateMachine.scala new file mode 100644 index 0000000..e188899 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForGuiStateMachine.scala @@ -0,0 +1,14 @@ +package de.retest.guistatemachine.rest.json + +import de.retest.guistatemachine.api.GuiStateMachine +import de.retest.guistatemachine.api.impl.GuiStateMachineImpl +import spray.json.{JsObject, JsValue, RootJsonFormat} + +class JsonFormatForGuiStateMachine extends RootJsonFormat[GuiStateMachine] { + + // TODO #1 Convert a GuiStateMachine into JSON + override def write(obj: GuiStateMachine): JsValue = JsObject() + + // TODO #1 Convert JSON into GuiStateMachine + override def read(json: JsValue): GuiStateMachine = new GuiStateMachineImpl() +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForState.scala b/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForState.scala new file mode 100644 index 0000000..201e48a --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/json/JsonFormatForState.scala @@ -0,0 +1,14 @@ +package de.retest.guistatemachine.rest.json + +import de.retest.guistatemachine.api.State +import de.retest.guistatemachine.api.impl.StateImpl +import spray.json.{JsObject, JsValue, RootJsonFormat} + +class JsonFormatForState extends RootJsonFormat[State] { + + // TODO #1 Convert a State into JSON + override def write(obj: State): JsValue = JsObject() + + // TODO #1 Convert JSON into State + override def read(json: JsValue): State = throw new RuntimeException("Implementation is missing!") +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Action.scala b/src/main/scala/de/retest/guistatemachine/rest/model/Action.scala deleted file mode 100644 index c027392..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Action.scala +++ /dev/null @@ -1,3 +0,0 @@ -package de.retest.guistatemachine.rest.model - -case class Action() diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Actions.scala b/src/main/scala/de/retest/guistatemachine/rest/model/Actions.scala deleted file mode 100644 index 64c6825..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Actions.scala +++ /dev/null @@ -1,3 +0,0 @@ -package de.retest.guistatemachine.rest.model - -case class Actions(actions: Map[Action]) diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/State.scala b/src/main/scala/de/retest/guistatemachine/rest/model/State.scala deleted file mode 100644 index 148cb56..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/model/State.scala +++ /dev/null @@ -1,3 +0,0 @@ -package de.retest.guistatemachine.rest.model - -final case class State(transitions: Transitions) diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/StateMachine.scala b/src/main/scala/de/retest/guistatemachine/rest/model/StateMachine.scala deleted file mode 100644 index 45f1094..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/model/StateMachine.scala +++ /dev/null @@ -1,6 +0,0 @@ -package de.retest.guistatemachine.rest.model - -/** - * State machine which represents a GUI test. - */ -final case class StateMachine(states: States = States(Map.fromValues[State](State(Transitions()))), actions: Actions = Actions(Map())) diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/StateMachines.scala b/src/main/scala/de/retest/guistatemachine/rest/model/StateMachines.scala deleted file mode 100644 index 9fc6200..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/model/StateMachines.scala +++ /dev/null @@ -1,10 +0,0 @@ -package de.retest.guistatemachine.rest.model - -import io.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field - -@ApiModel(description = "A map of state machines") -final case class StateMachines( - @(ApiModelProperty @field)(value = "A map of state machines") - stateMachines: Map[StateMachine]) diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/States.scala b/src/main/scala/de/retest/guistatemachine/rest/model/States.scala deleted file mode 100644 index 16f4859..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/model/States.scala +++ /dev/null @@ -1,3 +0,0 @@ -package de.retest.guistatemachine.rest.model - -case class States(states: Map[State]) diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Transition.scala b/src/main/scala/de/retest/guistatemachine/rest/model/Transition.scala deleted file mode 100644 index 5a6df45..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Transition.scala +++ /dev/null @@ -1,3 +0,0 @@ -package de.retest.guistatemachine.rest.model - -final case class Transition(to: Id, action: Id) diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Transitions.scala b/src/main/scala/de/retest/guistatemachine/rest/model/Transitions.scala deleted file mode 100644 index 37b259a..0000000 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Transitions.scala +++ /dev/null @@ -1,3 +0,0 @@ -package de.retest.guistatemachine.rest.model - -final case class Transitions(transitions: Map[Transition] = Map()) diff --git a/src/test/scala/de/retest/guistatemachine/rest/model/IdSpec.scala b/src/test/scala/de/retest/guistatemachine/api/IdSpec.scala similarity index 86% rename from src/test/scala/de/retest/guistatemachine/rest/model/IdSpec.scala rename to src/test/scala/de/retest/guistatemachine/api/IdSpec.scala index a0bc895..d49cb86 100644 --- a/src/test/scala/de/retest/guistatemachine/rest/model/IdSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/IdSpec.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine.rest.model +package de.retest.guistatemachine.api import org.scalatest.{Matchers, WordSpec} diff --git a/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImplSpec.scala b/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImplSpec.scala index c273c3d..6fe082c 100644 --- a/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImplSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImplSpec.scala @@ -1,19 +1,28 @@ package de.retest.guistatemachine.api.impl -import de.retest.guistatemachine.api.{AbstractApiSpec, GuiStateMachine} +import de.retest.guistatemachine.api.{AbstractApiSpec, Id} class GuiStateMachineApiImplSpec extends AbstractApiSpec { - var stateMachine: GuiStateMachine = null + var stateMachineId = Id(-1) "GuiStateMachineApi" should { "create a new state machine" in { - stateMachine = GuiStateMachineApiImpl.createStateMachine - stateMachine should not be null + stateMachineId = GuiStateMachineApiImpl.createStateMachine + stateMachineId shouldEqual Id(0) + } + + "get a state machine" in { + val stateMachine = GuiStateMachineApiImpl.getStateMachine(stateMachineId) + stateMachine.isDefined shouldBe true + val fsm = stateMachine.get + fsm.getActionExecutionTimes.size shouldEqual 0 + fsm.getAllExploredActions.size shouldEqual 0 + fsm.getAllNeverExploredActions.size shouldEqual 0 } "remove a state machine" in { - GuiStateMachineApiImpl.removeStateMachine(stateMachine) shouldBe true + GuiStateMachineApiImpl.removeStateMachine(stateMachineId) shouldBe true } } } diff --git a/src/test/scala/de/retest/guistatemachine/rest/model/MapSpec.scala b/src/test/scala/de/retest/guistatemachine/api/impl/IdMapSpec.scala similarity index 63% rename from src/test/scala/de/retest/guistatemachine/rest/model/MapSpec.scala rename to src/test/scala/de/retest/guistatemachine/api/impl/IdMapSpec.scala index 6d990eb..8901701 100644 --- a/src/test/scala/de/retest/guistatemachine/rest/model/MapSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/impl/IdMapSpec.scala @@ -1,15 +1,16 @@ -package de.retest.guistatemachine.rest.model +package de.retest.guistatemachine.api.impl +import de.retest.guistatemachine.api.Id import org.scalatest.{Matchers, WordSpec} import scala.collection.immutable.HashMap -class MapSpec extends WordSpec with Matchers { +class IdMapSpec extends WordSpec with Matchers { - "Map" should { + "IdMapSpec" should { "generate new IDs" in { val hashMap = new HashMap[Id, Int] - val map = Map(hashMap) + val map = IdMap(hashMap) val id0 = map.generateId map.values = hashMap + (id0 -> 1) val id1 = map.generateId diff --git a/src/test/scala/de/retest/guistatemachine/rest/JsonFormatForIdMapSpec.scala b/src/test/scala/de/retest/guistatemachine/rest/JsonFormatForIdMapSpec.scala deleted file mode 100644 index f2bed4d..0000000 --- a/src/test/scala/de/retest/guistatemachine/rest/JsonFormatForIdMapSpec.scala +++ /dev/null @@ -1,37 +0,0 @@ -package de.retest.guistatemachine.rest - -import de.retest.guistatemachine.rest.model.{Action, Actions, Id} -import org.scalatest.{Matchers, WordSpec} -import spray.json.DefaultJsonProtocol._ -import spray.json._ - -import scala.collection.immutable.HashMap - -class JsonFormatForIdMapSpec extends WordSpec with Matchers { - implicit val idFormat = jsonFormat1(Id) - - implicit val actionFormat = jsonFormat0(Action) - implicit val idMapFormatActions = new JsonFormatForIdMap[Action] - implicit val actionsFormat = jsonFormat1(Actions) - - "The JSON format" should { - "convert empty actions into JSON and back" in { - val actions = model.Actions(model.Map(new HashMap[Id, Action]())) - val json = actions.toJson - json.toString shouldEqual "{\"actions\":{}}" - val transformedActions = json.convertTo[Actions] - transformedActions.actions.values.isEmpty shouldEqual true - } - - "convert actions with elements into JSON and back" in { - val actions = model.Actions(model.Map(new HashMap[Id, Action]())) - actions.actions.values = actions.actions.values + (Id(0) -> Action()) - val json = actions.toJson - json.toString shouldEqual "{\"actions\":{\"0\":{}}}" - val transformedActions = json.convertTo[Actions] - transformedActions.actions.values.isEmpty shouldEqual false - transformedActions.actions.values.contains(Id(0)) shouldEqual true - } - } - -} diff --git a/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala b/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala index 5fcc331..fd55c10 100644 --- a/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala @@ -3,13 +3,15 @@ package de.retest.guistatemachine.rest import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import akka.http.scaladsl.model.{MediaTypes, StatusCodes} import akka.http.scaladsl.testkit.ScalatestRouteTest -import de.retest.guistatemachine.persistence.Persistence +import de.retest.guistatemachine.rest.json.DefaultJsonFormats import org.scalatest.{Matchers, WordSpec} +import de.retest.guistatemachine.api.impl.GuiStateMachineApiImpl +import de.retest.guistatemachine.api.GuiStateMachine +import de.retest.guistatemachine.api.Id class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest with RestService with DefaultJsonFormats { - val persistence = new Persistence - val sut = getRoute(persistence) + val sut = getRoute(GuiStateMachineApiImpl) "The service" should { "show the default text for the GET request with the path /" in { @@ -20,16 +22,6 @@ class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest wit } } - "return an empty list for the GET request with the path /state-machines" in { - Get("/state-machines") ~> sut ~> check { - import de.retest.guistatemachine.rest.model.StateMachines - handled shouldEqual true - mediaType shouldEqual MediaTypes.`application/json` - val r = responseAs[StateMachines] - r.stateMachines.values.size shouldEqual 0 - } - } - "fail for the GET request with the path /state-machine/0" in { Get("/state-machine/0") ~> sut ~> check { handled shouldEqual true @@ -46,21 +38,20 @@ class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest wit "allow POST for path /state-machine" in { Post("/state-machine") ~> sut ~> check { - import de.retest.guistatemachine.rest.model.Id handled shouldEqual true responseAs[Id] shouldEqual Id(0) - persistence.getStateMachines().stateMachines.values.size shouldEqual 1 + GuiStateMachineApiImpl.getStateMachine(Id(0)).isDefined shouldEqual true } } "return an empty application for the GET request with the path /state-machine/0" in { Get("/state-machine/0") ~> sut ~> check { - import de.retest.guistatemachine.rest.model.StateMachine handled shouldEqual true status shouldEqual StatusCodes.OK - val r = responseAs[StateMachine] - r.states.states.values.size shouldEqual 1 - r.actions.actions.values.size shouldEqual 0 + val r = responseAs[GuiStateMachine] + r.getAllNeverExploredActions.size shouldEqual 0 + r.getAllExploredActions.size shouldEqual 0 + r.getActionExecutionTimes.size shouldEqual 0 } } @@ -69,17 +60,16 @@ class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest wit handled shouldEqual true status shouldEqual StatusCodes.OK responseAs[String] shouldEqual "OK" - persistence.getStateMachines().stateMachines.values.size shouldEqual 0 + GuiStateMachineApiImpl.getStateMachine(Id(0)).isEmpty shouldEqual true } } - "not handle the GET request with the path /state-machines/bla/hello/bla" in { - Get("/state-machines/bla/hello/bla") ~> sut ~> check { + "not handle the GET request with the path /state-machine/bla/hello/bla" in { + Get("/state-machine/bla/hello/bla") ~> sut ~> check { handled shouldEqual false - //mediaType shouldEqual MediaTypes.`application/json` - //val r = responseAs[GuiApplications] - //r.apps.values.size shouldEqual 0 } } + + // TODO #1 Test getting states and executing actions } }