diff --git a/README.md b/README.md index ab8b62a..d2916dd 100644 --- a/README.md +++ b/README.md @@ -65,5 +65,9 @@ Some suggestions how the REST API for the state machine could look like: * `/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 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 diff --git a/build.sbt b/build.sbt index 6117e06..a2bdea2 100644 --- a/build.sbt +++ b/build.sbt @@ -7,8 +7,8 @@ organization := "retest" scalaVersion := "2.12.7" // Dependencies to represent the input of states and actions: -libraryDependencies += "de.retest" % "retest-model" % "5.0.0" withSources() withJavadoc() -libraryDependencies += "org.seleniumhq.selenium" % "selenium-java" % "2.35.0" withSources() withJavadoc() +libraryDependencies += "de.retest" % "retest-model" % "5.0.0" withSources () withJavadoc () +libraryDependencies += "org.seleniumhq.selenium" % "selenium-java" % "2.35.0" withSources () withJavadoc () // Dependencies to provide a REST service: libraryDependencies += "com.github.scopt" % "scopt_2.12" % "3.7.0" @@ -20,6 +20,10 @@ libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.5" libraryDependencies += "com.typesafe.akka" %% "akka-http-xml" % "10.1.5" libraryDependencies += "com.typesafe.akka" %% "akka-http-testkit" % "10.1.5" % "test" +// Swagger: +libraryDependencies += "io.swagger" % "swagger-jaxrs" % "1.5.21" +libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "1.0.0" + // Test frameworks: libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test" libraryDependencies += "org.scalamock" %% "scalamock" % "4.1.0" % "test" @@ -29,12 +33,5 @@ mainClass in (Compile, run) := Some("de.retest.guistatemachine.rest.WebServer") // set the main class for packaging the main jar mainClass in (Compile, packageBin) := Some("de.retest.guistatemachine.rest.WebServer") -publishTo := { - val nexus = "https://oss.sonatype.org/" - if (isSnapshot.value) - Some("snapshots" at nexus + "content/repositories/snapshots") - else - Some("releases" at nexus + "service/local/staging/deploy/maven2") -} - -credentials += Credentials(Path.userHome / ".sbt" / ".credentials") \ No newline at end of file +// format the code +scalafmtOnCompile := true diff --git a/project/assembly.sbt b/project/assembly.sbt deleted file mode 100644 index 09c90ca..0000000 --- a/project/assembly.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..2974f79 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,10 @@ +// to create a standalone JAR file with all dependencies +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") +// code formatting +addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15") +// for signed releases +addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") +addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4") +addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.10") +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") \ No newline at end of file diff --git a/project/sbteclipse.sbt b/project/sbteclipse.sbt deleted file mode 100644 index 7a2dbb3..0000000 --- a/project/sbteclipse.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "5.2.4") \ No newline at end of file diff --git a/project/sbtrelease.sbt b/project/sbtrelease.sbt deleted file mode 100644 index 1654adf..0000000 --- a/project/sbtrelease.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.10") \ No newline at end of file diff --git a/project/scalafmt.sbt b/project/scalafmt.sbt deleted file mode 100644 index 2917c4e..0000000 --- a/project/scalafmt.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") \ No newline at end of file diff --git a/project/scalastyle.sbt b/project/scalastyle.sbt deleted file mode 100644 index 834ed0a..0000000 --- a/project/scalastyle.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") \ No newline at end of file diff --git a/project/scoverage.sbt b/project/scoverage.sbt deleted file mode 100644 index 4e88803..0000000 --- a/project/scoverage.sbt +++ /dev/null @@ -1 +0,0 @@ -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") \ No newline at end of file diff --git a/scalastyle-config.xml b/scalastyle-config.xml index ead98da..3432d70 100644 --- a/scalastyle-config.xml +++ b/scalastyle-config.xml @@ -112,6 +112,6 @@ - - + + \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/api/Action.scala b/src/main/scala/de/retest/guistatemachine/api/Action.scala index e3432e4..7d81d28 100644 --- a/src/main/scala/de/retest/guistatemachine/api/Action.scala +++ b/src/main/scala/de/retest/guistatemachine/api/Action.scala @@ -8,4 +8,4 @@ case class Action(a: org.openqa.selenium.interactions.Action) { // TODO #6 Convert abstract representation of actions into string. override def toString: String = "Selenium Action" -} \ No newline at end of file +} diff --git a/src/main/scala/de/retest/guistatemachine/api/ActionTransitions.scala b/src/main/scala/de/retest/guistatemachine/api/ActionTransitions.scala new file mode 100644 index 0000000..2dee01f --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/ActionTransitions.scala @@ -0,0 +1,11 @@ +package de.retest.guistatemachine.api + +/** + * Represents transitions for one single symbol which is represented by an [[Action]] to a number of states. + * The corresponding symbol is not stored in this class but in the [[State]] from which the transitions are started. + * + * @param to The states which the transitions lead to. Since it is a NFA, there can be multiple states for the same symbol. + * @param executionCounter The number of times all transitions for the action have been executed from the corresponding state. + * It does not matter to which state. In the legacy code this was stored as `StateGraph.executionCounter`. + */ +case class ActionTransitions(to: Set[State], executionCounter: Int) diff --git a/src/main/scala/de/retest/guistatemachine/api/Descriptors.scala b/src/main/scala/de/retest/guistatemachine/api/Descriptors.scala index d52701e..8a9d050 100644 --- a/src/main/scala/de/retest/guistatemachine/api/Descriptors.scala +++ b/src/main/scala/de/retest/guistatemachine/api/Descriptors.scala @@ -5,4 +5,4 @@ import de.retest.ui.descriptors.RootElement /** * Set of root elements which identifies a state. */ -case class Descriptors(rootElements: Set[RootElement]) \ No newline at end of file +case class Descriptors(rootElements: Set[RootElement]) diff --git a/src/main/scala/de/retest/guistatemachine/api/GuiStateMachine.scala b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachine.scala index 145f8a8..2b952f5 100644 --- a/src/main/scala/de/retest/guistatemachine/api/GuiStateMachine.scala +++ b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachine.scala @@ -4,12 +4,15 @@ package de.retest.guistatemachine.api * API to create a NFA which represents the current state machine of an automatic GUI test generation with the help of a genetic algorithm. * Simulated actions by the user are mapped to transitions in the state machine. * States are identified by descriptors. - * There can be ambigious states which makes the finite state machine non-deterministic. + * There can be ambiguous states which makes the finite state machine non-deterministic. + * There can also be multiple start states for NFAs. + * Therefore, we do not provide any functionality to set or get the initial state. */ trait GuiStateMachine { /** * Gets a state identified by descriptors and with its initial never explored actions. + * * @param descriptors The descriptors which identify the state. * @param neverExploredActions All actions which have never been explored from the state. * @return The state identified by the descriptors. If there has not been any state yet, it will be added. @@ -29,13 +32,23 @@ trait GuiStateMachine { /** * Can be used by the GA to generate new test cases. + * * @return All actions which have not been explored yet. */ - def getAllNeverExploredActions: scala.collection.mutable.Set[Action] + def getAllNeverExploredActions: Set[Action] /** * In the legacy code this was only used to show the number of actions which have been explored by Monkey Testing. + * * @return All actions which have been explored and therefore have a corresponding transition. */ - def getAllExploredActions: scala.collection.mutable.Set[Action] -} \ No newline at end of file + def getAllExploredActions: Set[Action] + + /** + * In the legacy code this was only used to calculate [[getAllNeverExploredActions]]. + * It could be used for the visualization of the NFA to see how often actions are executed. + * + * @return The number of times every explored action has been executed in the NFA. + */ + def getActionExecutionTimes: Map[Action, Int] +} diff --git a/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala index 9a88725..f3ed50e 100644 --- a/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala +++ b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachineApi.scala @@ -4,14 +4,28 @@ trait GuiStateMachineApi { /** * Creates a new [[GuiStateMachine]]. + * * @return The new GUI state machine. */ def createStateMachine: GuiStateMachine /** - * Removes a persisted [[GuiStateMachine]]. + * Removes an existing [[GuiStateMachine]]. + * * @param stateMachine The persisted GUI state machine. - * @return True if it has been persisted before and is no remove. Otherwise, false. + * @return True if it existed and was removed by this call. Otherwise, false. */ def removeStateMachine(stateMachine: GuiStateMachine): Boolean -} \ No newline at end of file + + /** + * Stores all state machines on the disk. + * Persistence can be useful when the state machines become quite big and the generation/modification is interrupted + * and continued later. + */ + def persist(): Unit + + /** + * Loads all state machines from the disk. + */ + def load(): Unit +} diff --git a/src/main/scala/de/retest/guistatemachine/api/State.scala b/src/main/scala/de/retest/guistatemachine/api/State.scala index 3c379e8..f8dbd0a 100644 --- a/src/main/scala/de/retest/guistatemachine/api/State.scala +++ b/src/main/scala/de/retest/guistatemachine/api/State.scala @@ -23,10 +23,11 @@ trait State { * Hence, we have a set of states per action. * In the legacy code there was a type called `AmbigueState` but a multimap simplifies the implementation. */ - def getTransitions: Map[Action, Set[State]] + def getTransitions: Map[Action, ActionTransitions] /** * This was used in the legacy code for Monkey testing. + * * @return Returns a random action or an empty value if there are none left. */ def getRandomAction(): Option[Action] = { @@ -37,19 +38,6 @@ trait State { } } - /** - * Adds a new transition to the state which is only allowed by calling the methods of [[GuiStateMachine]]. - * @param a The action which represents the transition's consumed symbol. - * @param to The state which the transition leads t o. - */ - private[api] def addTransition(a: Action, to: State): Unit - - /** - * This was named `getRandomActions` in the legacy code but actually returned all actions. - * @return All actions (explored + unexplored). - */ - private def getAllActions(): Set[Action] = getNeverExploredActions ++ getTransitions.keySet - /** * Overriding this method is required to allow the usage of a set of states. * Comparing the descriptors should check for the equality of all root elements which compares the identifying attributes and the contained components @@ -67,4 +55,20 @@ trait State { override def hashCode(): Int = this.getDescriptors.hashCode() override def toString: String = s"descriptors=${getDescriptors},neverExploredActions=${getNeverExploredActions},transitions=${getTransitions}" -} \ No newline at end of file + + /** + * Adds a new transition to the state which is only allowed by calling the methods of [[GuiStateMachine]]. + * + * @param a The action which represents the transition's consumed symbol. + * @param to The state which the transition leads t o. + * @return The number of times the action has been executed from this state. The target state does not matter for this number. + */ + private[api] def addTransition(a: Action, to: State): Int + + /** + * This was named `getRandomActions` in the legacy code but actually returned all actions. + * + * @return All actions (explored + unexplored). + */ + private def getAllActions(): Set[Action] = getNeverExploredActions ++ getTransitions.keySet +} 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 31f5532..17db53a 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImpl.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImpl.scala @@ -15,4 +15,12 @@ object GuiStateMachineApiImpl extends GuiStateMachineApi { } override def removeStateMachine(stateMachine: GuiStateMachine): Boolean = stateMachines.remove(stateMachine) -} \ No newline at end of file + + override def persist(): Unit = { + // TODO #9 store on the disk + } + + override def load(): Unit = { + // TODO #9 Load from 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 f852863..9e384e0 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala @@ -1,44 +1,55 @@ package de.retest.guistatemachine.api.impl import de.retest.guistatemachine.api.{Action, Descriptors, GuiStateMachine, State} - -import scala.collection.mutable.HashMap -import scala.collection.mutable.HashSet +import scala.collection.immutable.{HashMap, HashSet} class GuiStateMachineImpl extends GuiStateMachine { - val states = new HashMap[Descriptors, State] + var states = new HashMap[Descriptors, State] /** * In the legacy code we had `getAllNeverExploredActions` which had to collect them from all states and make sure they were never executed. * Storing them directly in a set improves efficiency. */ - val allNeverExploredActions = new HashSet[Action] + var allNeverExploredActions = new HashSet[Action] /** * The legacy code stored execution counters for every action. */ - val allExploredActions = new HashSet[Action] + var allExploredActions = new HashSet[Action] + + /** + * `actionExecutionCounter` from the legacy code. + * Stores the total number of executions per action. + */ + var actionExecutionTimes = new HashMap[Action, Int] override def getState(descriptors: Descriptors, neverExploredActions: Set[Action]): State = { if (states.contains(descriptors)) { states(descriptors) } else { - allNeverExploredActions ++= (neverExploredActions -- allExploredActions) + allNeverExploredActions = allNeverExploredActions ++ (neverExploredActions -- allExploredActions) val s = new StateImpl(descriptors, neverExploredActions) - states += (descriptors -> s) + states = states + (descriptors -> s) s } } override def executeAction(from: State, a: Action, descriptors: Descriptors, neverExploredActions: Set[Action]): State = { val to = getState(descriptors, neverExploredActions) - allExploredActions += a - allNeverExploredActions -= a + allExploredActions = allExploredActions + a + allNeverExploredActions = allNeverExploredActions - a + val old = actionExecutionTimes.get(a) + old match { + case Some(o) => actionExecutionTimes = actionExecutionTimes + (a -> (o + 1)) + case None => actionExecutionTimes = actionExecutionTimes + (a -> 1) + } from.addTransition(a, to) to } - override def getAllNeverExploredActions: scala.collection.mutable.Set[Action] = allNeverExploredActions + override def getAllNeverExploredActions: Set[Action] = allNeverExploredActions + + override def getAllExploredActions: Set[Action] = allExploredActions - override def getAllExploredActions: scala.collection.mutable.Set[Action] = allExploredActions -} \ No newline at end of file + override def getActionExecutionTimes: Map[Action, Int] = actionExecutionTimes +} diff --git a/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala b/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala index aed3817..db2fc00 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala @@ -1,28 +1,36 @@ package de.retest.guistatemachine.api.impl -import de.retest.guistatemachine.api.{Action, Descriptors, State} +import de.retest.guistatemachine.api.{Action, ActionTransitions, Descriptors, State} -import scala.collection.immutable.{HashMap, HashSet} +import scala.collection.immutable.HashMap -class StateImpl(val descriptors: Descriptors, var neverExploredActions: Set[Action]) extends State { +class StateImpl(descriptors: Descriptors, var neverExploredActions: Set[Action]) extends State { /** * TODO #4 Currently, there is no MultiMap trait for immutable maps in the Scala standard library. * The legacy code used `AmbigueState` here which was more complicated than just a multi map. */ - var transitions = new HashMap[Action, Set[State]] + var transitions = new HashMap[Action, ActionTransitions] override def getDescriptors: Descriptors = descriptors override def getNeverExploredActions: Set[Action] = neverExploredActions - override def getTransitions: Map[Action, Set[State]] = transitions + override def getTransitions: Map[Action, ActionTransitions] = transitions - private[api] override def addTransition(a: Action, to: State): Unit = { - if (!transitions.contains(a)) { - transitions = transitions + (a -> HashSet(to)) - // In the legacy code this is done in `increaseTimesExecuted`. - neverExploredActions = neverExploredActions - a - } else { - transitions = transitions + (a -> (transitions(a) + to)) + private[api] override def addTransition(a: Action, to: State): Int = { + val old = transitions.get(a) + old match { + case Some(o) => { + val updated = ActionTransitions(o.to + to, o.executionCounter + 1) + transitions = transitions + (a -> updated) + updated.executionCounter + } + + case None => { + transitions += (a -> ActionTransitions(Set(to), 1)) + // In the legacy code this is done in `increaseTimesExecuted`. + neverExploredActions = neverExploredActions - a + 1 + } } } -} \ No newline at end of file +} diff --git a/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala b/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala index 94ddb7c..18275a9 100644 --- a/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala +++ b/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala @@ -16,4 +16,4 @@ class Persistence { def createStateMachine(): Id = stateMachines.stateMachines.addNewElement(StateMachine()) def deleteStateMachine(id: Id): Boolean = stateMachines.stateMachines.removeElement(id) -} \ No newline at end of file +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/DefaultJsonFormats.scala b/src/main/scala/de/retest/guistatemachine/rest/DefaultJsonFormats.scala new file mode 100644 index 0000000..46cbc7a --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/DefaultJsonFormats.scala @@ -0,0 +1,21 @@ +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/JsonFormatForIdMap.scala b/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala index 0194428..91eef8c 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala @@ -24,4 +24,4 @@ class JsonFormatForIdMap[T](implicit (Id(x._1.toLong) -> x._2) }) } -} \ No newline at end of file +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/RestService.scala b/src/main/scala/de/retest/guistatemachine/rest/RestService.scala index e73822a..55544e4 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/RestService.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/RestService.scala @@ -1,60 +1,19 @@ package de.retest.guistatemachine.rest -import akka.actor.ActorSystem -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ -import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import akka.stream.ActorMaterializer import de.retest.guistatemachine.persistence.Persistence -import de.retest.guistatemachine.rest.model.{Action, Actions, Id, State, StateMachine, StateMachines, States, Transition, Transitions} -import spray.json.DefaultJsonProtocol._ trait RestService { - implicit val system: ActorSystem - implicit val materializer: ActorMaterializer + def getRoute(persistence: Persistence): Route = { + val stateMachinesService = new StateMachinesService(persistence) + val stateMachineService = new StateMachineService(persistence) - // 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) - - def getRoute(persistence: Persistence): Route = get { pathSingleSlash { complete("GUI State Machine API") - } ~ - path("state-machines") { - complete(persistence.getStateMachines()) - } ~ - path("state-machine" / LongNumber) { id => - val app = persistence.getStateMachine(Id(id)) - app match { - case Some(x) => complete(x) - case None => complete(StatusCodes.NotFound) - } - } - } ~ - post { - path("create-state-machine") { - val id = persistence.createStateMachine() - complete(id) - } - } ~ delete { - path("state-machine" / LongNumber) { stateMachineId => - val r = persistence.deleteStateMachine(Id(stateMachineId)) - complete(if (r) StatusCodes.OK else StatusCodes.NotFound) } - } -} \ No newline at end of file + } ~ stateMachinesService.getRoute() ~ stateMachineService.getRoute() ~ getFromResourceDirectory("swagger") ~ 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 new file mode 100644 index 0000000..4ecb5b0 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/StateMachineService.scala @@ -0,0 +1,51 @@ +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 new file mode 100644 index 0000000..fd6c334 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/StateMachinesService.scala @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..bdae799 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/SwaggerDocService.scala @@ -0,0 +1,14 @@ +package de.retest.guistatemachine.rest + +import com.github.swagger.akka.SwaggerHttpService +import com.github.swagger.akka.model.Info +import io.swagger.models.ExternalDocs + +object SwaggerDocService extends SwaggerHttpService { + override val apiClasses = Set(classOf[RestService]) + 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")) + //override val securitySchemeDefinitions = Map("basicAuth" -> new BasicAuthDefinition()) + override val unwantedDefinitions = Seq("Function1", "Function1RequestContextFutureRouteResult") +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala b/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala index 3a261ea..0d14f56 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala @@ -20,7 +20,7 @@ object WebServer extends App with RestService { case class Config(maxtime: Long = -1) - val parser = new OptionParser[Config]("scopt") { + val parser = new OptionParser[Config]("gui-state-machine") { head("gui-state-machine-api", "1.0") opt[Long]('t', "maxtime") action { (x, c) => @@ -50,4 +50,4 @@ object WebServer extends App with RestService { .onComplete(_ => system.terminate()) // and shutdown when done case None => println("Missing config.") } -} \ No newline at end of file +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Action.scala b/src/main/scala/de/retest/guistatemachine/rest/model/Action.scala index fe5a367..c027392 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Action.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/Action.scala @@ -1,3 +1,3 @@ package de.retest.guistatemachine.rest.model -case class Action() \ No newline at end of file +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 index ef15e6c..64c6825 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Actions.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/Actions.scala @@ -1,3 +1,3 @@ package de.retest.guistatemachine.rest.model -case class Actions(actions: Map[Action]) \ No newline at end of file +case class Actions(actions: Map[Action]) diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Id.scala b/src/main/scala/de/retest/guistatemachine/rest/model/Id.scala index 7c898e2..f3ea467 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Id.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/Id.scala @@ -3,4 +3,4 @@ package de.retest.guistatemachine.rest.model final case class Id(val id: Long) extends Ordered[Id] { override def compare(that: Id): Int = this.id compare that.id -} \ No newline at end of file +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/Map.scala b/src/main/scala/de/retest/guistatemachine/rest/model/Map.scala index d6974db..599bae8 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Map.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/Map.scala @@ -39,4 +39,4 @@ object Map { for (e <- v) r.addNewElement(e) r } -} \ No newline at end of file +} diff --git a/src/main/scala/de/retest/guistatemachine/rest/model/State.scala b/src/main/scala/de/retest/guistatemachine/rest/model/State.scala index 2282f7d..148cb56 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/State.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/State.scala @@ -1,3 +1,3 @@ package de.retest.guistatemachine.rest.model -final case class State(transitions: Transitions) \ No newline at end of file +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 index 1706747..45f1094 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/StateMachine.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/StateMachine.scala @@ -1,6 +1,6 @@ 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())) \ No newline at end of file + * 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 index c48c492..9fc6200 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/StateMachines.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/StateMachines.scala @@ -1,3 +1,10 @@ package de.retest.guistatemachine.rest.model -final case class StateMachines(stateMachines: Map[StateMachine]) \ No newline at end of file +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 index 9f18dc9..16f4859 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/States.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/States.scala @@ -1,3 +1,3 @@ package de.retest.guistatemachine.rest.model -case class States(states: Map[State]) \ No newline at end of file +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 index 4e3965a..5a6df45 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Transition.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/Transition.scala @@ -1,3 +1,3 @@ package de.retest.guistatemachine.rest.model -final case class Transition(to : Id, action : Id) \ No newline at end of file +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 index 54da2ad..37b259a 100644 --- a/src/main/scala/de/retest/guistatemachine/rest/model/Transitions.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/model/Transitions.scala @@ -1,3 +1,3 @@ package de.retest.guistatemachine.rest.model -final case class Transitions(transitions: Map[Transition] = Map()) \ No newline at end of file +final case class Transitions(transitions: Map[Transition] = Map()) diff --git a/src/test/scala/de/retest/guistatemachine/api/AbstractApiSpec.scala b/src/test/scala/de/retest/guistatemachine/api/AbstractApiSpec.scala index b83acfd..33fa97d 100644 --- a/src/test/scala/de/retest/guistatemachine/api/AbstractApiSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/AbstractApiSpec.scala @@ -7,10 +7,11 @@ import de.retest.ui.image.Screenshot import org.scalamock.scalatest.MockFactory import org.scalatest.{Matchers, WordSpec} -abstract class AbstractApiSpec extends WordSpec with Matchers with MockFactory { +abstract trait AbstractApiSpec extends WordSpec with Matchers with MockFactory { /** * Creates a new identifying attributes collection which should only match other identifying attributes with the same ID. + * * @param id The ID is used as value for different attributes. * @return The identifying attributes. */ @@ -19,6 +20,7 @@ abstract class AbstractApiSpec extends WordSpec with Matchers with MockFactory { /** * The identifying attributes and the contained components specify the equality. + * * @param id If the ID is equal the returned root element will be equal. * @return A new root element which is equal to itself but not to any other root element. */ 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 4329219..c273c3d 100644 --- a/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImplSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImplSpec.scala @@ -16,4 +16,4 @@ class GuiStateMachineApiImplSpec extends AbstractApiSpec { GuiStateMachineApiImpl.removeStateMachine(stateMachine) shouldBe true } } -} \ No newline at end of file +} diff --git a/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala b/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala index 4ca2835..7a5c467 100644 --- a/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala @@ -17,39 +17,49 @@ class GuiStateMachineImplSpec extends AbstractApiSpec { val initial = sut.getState(getDescriptors, getNeverExploredActions) sut.getAllExploredActions.size shouldEqual 0 sut.getAllNeverExploredActions.size shouldEqual 2 + sut.getActionExecutionTimes.size shouldEqual 0 // execute action0Mock for the first time val s0Descriptors = Descriptors(Set(rootElementA)) val s0 = sut.executeAction(initial, Action(action0Mock), s0Descriptors, getNeverExploredActions) initial.getNeverExploredActions.size shouldEqual 1 initial.getTransitions.size shouldEqual 1 - initial.getTransitions(Action(action0Mock)).size shouldEqual 1 + initial.getTransitions(Action(action0Mock)).to.size shouldEqual 1 + initial.getTransitions(Action(action0Mock)).executionCounter shouldEqual 1 s0.getNeverExploredActions.size shouldEqual 2 s0.getTransitions.size shouldEqual 0 sut.getAllExploredActions.size shouldEqual 1 sut.getAllNeverExploredActions.size shouldEqual 1 + sut.getActionExecutionTimes.get(Action(action0Mock)).isDefined shouldEqual true + sut.getActionExecutionTimes.get(Action(action0Mock)).get shouldEqual 1 // execute action0Mock for the second time val s1Descriptors = Descriptors(Set(rootElementB)) val s1 = sut.executeAction(initial, Action(action0Mock), s1Descriptors, getNeverExploredActions) initial.getNeverExploredActions.size shouldEqual 1 initial.getTransitions.size shouldEqual 1 - initial.getTransitions(Action(action0Mock)).size shouldEqual 2 + initial.getTransitions(Action(action0Mock)).to.size shouldEqual 2 + initial.getTransitions(Action(action0Mock)).executionCounter shouldEqual 2 s1.getNeverExploredActions.size shouldEqual 2 s1.getTransitions.size shouldEqual 0 sut.getAllExploredActions.size shouldEqual 1 sut.getAllNeverExploredActions.size shouldEqual 1 + sut.getActionExecutionTimes.get(Action(action0Mock)).isDefined shouldEqual true + sut.getActionExecutionTimes.get(Action(action0Mock)).get shouldEqual 2 // execute action1Mock for the first time val s2Descriptors = Descriptors(Set(rootElementC)) val s2 = sut.executeAction(initial, Action(action1Mock), s2Descriptors, getNeverExploredActions) initial.getNeverExploredActions.size shouldEqual 0 initial.getTransitions.size shouldEqual 2 - initial.getTransitions(Action(action1Mock)).size shouldEqual 1 + initial.getTransitions(Action(action1Mock)).to.size shouldEqual 1 + initial.getTransitions(Action(action1Mock)).executionCounter shouldEqual 1 s2.getNeverExploredActions.size shouldEqual 2 s2.getTransitions.size shouldEqual 0 sut.getAllExploredActions.size shouldEqual 2 sut.getAllNeverExploredActions.size shouldEqual 0 + sut.getActionExecutionTimes.get(Action(action1Mock)).isDefined shouldEqual true + sut.getActionExecutionTimes.get(Action(action1Mock)).get shouldEqual 1 } "store a state for the second access" in { @@ -62,4 +72,4 @@ class GuiStateMachineImplSpec extends AbstractApiSpec { def getDescriptors: Descriptors = Descriptors(Set(rootElementA, rootElementB, rootElementC)) def getNeverExploredActions: Set[Action] = Set(Action(action0Mock), Action(action1Mock)) -} \ No newline at end of file +} diff --git a/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala b/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala index 54a0ae6..71efc21 100644 --- a/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala @@ -15,12 +15,14 @@ class StateImplSpec extends AbstractApiSpec { val s1 = new StateImpl(descriptorsB, Set(Action(action1Mock))) s0.equals(s1) shouldEqual false s0.equals(null) shouldEqual false + s0.hashCode() should not equal s1.hashCode() } "equal" in { val s0 = new StateImpl(descriptorsA, Set(Action(action0Mock))) val s1 = new StateImpl(descriptorsA, Set(Action(action1Mock))) s0.equals(s1) shouldEqual true + s0.hashCode() shouldEqual s1.hashCode() } "be converted into a string" in { @@ -38,4 +40,4 @@ class StateImplSpec extends AbstractApiSpec { s0.getRandomAction().isEmpty shouldEqual true } } -} \ No newline at end of file +} diff --git a/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala b/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala index 6a585de..5fcc331 100644 --- a/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala @@ -6,7 +6,7 @@ import akka.http.scaladsl.testkit.ScalatestRouteTest import de.retest.guistatemachine.persistence.Persistence import org.scalatest.{Matchers, WordSpec} -class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest with RestService { +class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest with RestService with DefaultJsonFormats { val persistence = new Persistence val sut = getRoute(persistence) @@ -44,8 +44,8 @@ class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest wit } } - "allow POST for path /create-state-machine" in { - Post("/create-state-machine") ~> sut ~> check { + "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)