From d729a5116acd283680d29b337a478f625d282edf Mon Sep 17 00:00:00 2001 From: Tamino Dauth Date: Mon, 5 Nov 2018 17:04:28 +0100 Subject: [PATCH] Add API package and refactor code #3 #4 - Move REST part into package rest - Add api package and retest/Selenium dependencies - Use scalamock for mocking - Add scalafmt sbt plugin and rules file - Refactor DSL code --- .gitignore | 7 ++- .scalafmt.conf | 1 + README.md | 15 ++++-- build.sbt | 14 ++++-- project/scalafmt.sbt | 1 + .../guistatemachine/JsonFormatForIdMap.scala | 26 ---------- .../retest/guistatemachine/api/Action.scala | 6 +++ .../guistatemachine/api/Descriptors.scala | 8 ++++ .../guistatemachine/api/GuiStateMachine.scala | 29 ++++++++++++ .../de/retest/guistatemachine/api/State.scala | 15 ++++++ .../api/impl/GuiStateMachineImpl.scala | 25 ++++++++++ .../guistatemachine/api/impl/StateImpl.scala | 42 +++++++++++++++++ .../retest/guistatemachine/dsl/Action.scala | 2 +- .../guistatemachine/dsl/FinalState.scala | 4 +- .../guistatemachine/dsl/InitialState.scala | 5 +- .../de/retest/guistatemachine/dsl/State.scala | 13 ++++- .../guistatemachine/dsl/StateMachine.scala | 5 +- .../guistatemachine/dsl/StateMachines.scala | 2 +- .../guistatemachine/dsl/Transition.scala | 4 ++ .../rest/JsonFormatForIdMap.scala | 30 ++++++++++++ .../{ => rest}/RestService.scala | 2 +- .../{ => rest}/WebServer.scala | 7 +-- .../guistatemachine/api/AbstractApiSpec.scala | 40 ++++++++++++++++ .../guistatemachine/api/DescriptorsSpec.scala | 19 ++++++++ .../api/impl/GuiStateMachineImplSpec.scala | 31 ++++++++++++ .../api/impl/StateImplSpec.scala | 24 ++++++++++ .../dsl/StateMachinesSpec.scala | 47 ++++++++++++++----- .../{ => rest}/JsonFormatForIdMapSpec.scala | 2 +- .../{ => rest}/RestServiceSpec.scala | 2 +- .../{ => rest}/WebServerSpec.scala | 4 +- 30 files changed, 369 insertions(+), 63 deletions(-) create mode 100644 .scalafmt.conf create mode 100644 project/scalafmt.sbt delete mode 100644 src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/Action.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/Descriptors.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/GuiStateMachine.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/State.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala create mode 100644 src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala create mode 100644 src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala rename src/main/scala/de/retest/guistatemachine/{ => rest}/RestService.scala (99%) rename src/main/scala/de/retest/guistatemachine/{ => rest}/WebServer.scala (92%) create mode 100644 src/test/scala/de/retest/guistatemachine/api/AbstractApiSpec.scala create mode 100644 src/test/scala/de/retest/guistatemachine/api/DescriptorsSpec.scala create mode 100644 src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala create mode 100644 src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala rename src/test/scala/de/retest/guistatemachine/{ => rest}/JsonFormatForIdMapSpec.scala (98%) rename src/test/scala/de/retest/guistatemachine/{ => rest}/RestServiceSpec.scala (99%) rename src/test/scala/de/retest/guistatemachine/{ => rest}/WebServerSpec.scala (85%) diff --git a/.gitignore b/.gitignore index 7c4f5e1..3e82fc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -/bin/ -/target/ +/.classpath +/.settings +/.idea +/bin +/target /project/project /project/target \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..2aa31b5 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1 @@ +maxColumn = 160 \ No newline at end of file diff --git a/README.md b/README.md index f3ac2cf..49659b0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GUI State Machine API -REST service for the creation and modification of nondeterministic finite automaton of GUI tests based on a genetic algorithm. +REST service for the creation and modification of nondeterministic finite automaton for the automatic generation of GUI tests with the help of a genetic algorithm. The service hides the actual implementation and defines a fixed interface for calls. Therefore, calling systems do not depend on the concrete implementation and it can be mocked easily for tests. @@ -18,17 +18,23 @@ Therefore, calling systems do not depend on the concrete implementation and it c * `sbt coverageReport` to generate a HTML coverage report. * `sbt scalastyle` to make a check with ScalaStyle. * `sbt doc` to generate the scaladoc API documentation. +* `sbt scalafmt` to format the Scala source files with scalafmt. ## 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. ## NFA for the Representation of Tests A nondeterministic finite automaton represents the states of the GUI during the test. -The actions executed by the user on the widgets are the transitions. -If an action has not been executed, it leads to an unknown state. +The actions executed by the user on the widgets are represented by transitions. +If an action has not been executed yet from a state, it leads to an unknown state. +The unknown state is a special state from which all actions could be executed. The NFA is based on the UI model from [Search-Based System Testing: High Coverage, No False Alarms](http://www.specmate.org/papers/2012-07-Search-basedSystemTesting-HighCoverageNoFalseAlarms.pdf) (section "4.5 UI Model"). Whenever an unknown state is replaced by a newly discovered state, the NFA has to be updated. +The NFA is used to generate test cases (sequence of UI actions) with the help of a genetic algorithm. +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 @@ -47,6 +53,9 @@ 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. + ## DSL There is a DSL to construct an NFA with GUI actions manually. The package [dsl](./src/main/scala/de/retest/guistatemachine/dsl/). diff --git a/build.sbt b/build.sbt index 20233bc..e9ace65 100644 --- a/build.sbt +++ b/build.sbt @@ -2,10 +2,15 @@ name := "gui-state-machine-api" version := "1.0" -organization := "tdauth" +organization := "retest" scalaVersion := "2.12.7" +// Dependencies to represent the input of states and actions: +libraryDependencies += "de.retest" % "retest-model" % "5.0.0" +libraryDependencies += "org.seleniumhq.selenium" % "selenium-java" % "2.35.0" + +// Dependencies to provide a REST service: libraryDependencies += "com.github.scopt" % "scopt_2.12" % "3.7.0" libraryDependencies += "io.spray" % "spray-json_2.12" % "1.3.4" libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.1.5" @@ -14,9 +19,12 @@ libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.5.12" 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" + +// Test frameworks: libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test" +libraryDependencies += "org.scalamock" %% "scalamock" % "4.1.0" % "test" // set the main class for 'sbt run' -mainClass in (Compile, run) := Some("de.retest.guistatemachine.WebServer") +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.WebServer") \ No newline at end of file +mainClass in (Compile, packageBin) := Some("de.retest.guistatemachine.rest.WebServer") \ No newline at end of file diff --git a/project/scalafmt.sbt b/project/scalafmt.sbt new file mode 100644 index 0000000..2917c4e --- /dev/null +++ b/project/scalafmt.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1") \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala b/src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala deleted file mode 100644 index e37659c..0000000 --- a/src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala +++ /dev/null @@ -1,26 +0,0 @@ -package de.retest.guistatemachine - -import de.retest.guistatemachine.model.Id -import de.retest.guistatemachine.model.Map -import spray.json.JsValue -import spray.json.JsonFormat -import spray.json.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[Map[T]] { - - override def write(obj: Map[T]): JsValue = - jsonFormat0.write(obj.values.map { field => (field._1.id.toString -> field._2) }) - - override def read(json: JsValue): Map[T] = { - val map = jsonFormat0.read(json) - new Map[T](map.map { x => (Id(x._1.toLong) -> x._2) }) - } -} \ 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 new file mode 100644 index 0000000..f5ceecf --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/Action.scala @@ -0,0 +1,6 @@ +package de.retest.guistatemachine.api + +/** + * Interaction from the user with the GUI. + */ +case class Action(a : org.openqa.selenium.interactions.Action) \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/api/Descriptors.scala b/src/main/scala/de/retest/guistatemachine/api/Descriptors.scala new file mode 100644 index 0000000..8a9d050 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/Descriptors.scala @@ -0,0 +1,8 @@ +package de.retest.guistatemachine.api + +import de.retest.ui.descriptors.RootElement + +/** + * Set of root elements which identifies a state. + */ +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 new file mode 100644 index 0000000..8d73700 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/GuiStateMachine.scala @@ -0,0 +1,29 @@ +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. + */ +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. + */ + def getState(descriptors: Descriptors, neverExploredActions: Set[Action]): State + + /** + * Executes an action from a state leading to the current state described by descriptors. + * + * @param from The state the action is executed from + * @param a The action which is executed by the user. + * @param descriptors The descriptors which identify the state which the action leads to and which is returned by this method. + * @param neverExploredActions The never explored actions of the state which the action leads to and which is returned by this method. + * @return The current state which the transition of a leads to. + */ + def executeAction(from: State, a: Action, descriptors: Descriptors, neverExploredActions: Set[Action]): State +} diff --git a/src/main/scala/de/retest/guistatemachine/api/State.scala b/src/main/scala/de/retest/guistatemachine/api/State.scala new file mode 100644 index 0000000..1f7fdab --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/State.scala @@ -0,0 +1,15 @@ +package de.retest.guistatemachine.api + +/** + * A state should be identified by its corresponding [[Descriptors]]. + * It consists of actions which have not been explored yet and transitions which build up the state machine. + */ +trait State { + def getNeverExploredActions: Set[Action] + + /** + * NFA states can lead to different states by consuming the same symbol. + * Hence, we have a set of states per action. + */ + def getTransitions: Map[Action, Set[State]] +} diff --git a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala new file mode 100644 index 0000000..c4baed6 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala @@ -0,0 +1,25 @@ +package de.retest.guistatemachine.api.impl + +import de.retest.guistatemachine.api.{Action, Descriptors, GuiStateMachine, State} + +import scala.collection.mutable.HashMap + +object GuiStateMachineImpl extends GuiStateMachine { + val states = new HashMap[Descriptors, State] + + override def getState(descriptors: Descriptors, neverExploredActions: Set[Action]): State = { + if (states.contains(descriptors)) { + states(descriptors) + } else { + val s = new StateImpl(descriptors, neverExploredActions.to) + states += (descriptors -> s) + s + } + } + + override def executeAction(from: State, a: Action, descriptors: Descriptors, neverExploredActions: Set[Action]): State = { + val to = getState(descriptors, neverExploredActions) + from.asInstanceOf[StateImpl].addTransition(a, to) + to + } +} diff --git a/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala b/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala new file mode 100644 index 0000000..c92f0b5 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala @@ -0,0 +1,42 @@ +package de.retest.guistatemachine.api.impl + +import de.retest.guistatemachine.api.{Action, Descriptors, State} + +import scala.collection.immutable.{HashMap, HashSet} + +class StateImpl(val descriptors: Descriptors, var neverExploredActions: Set[Action]) extends State { + + /** + * TODO #4 Currently, there is no MultiMap trait for immutable maps. + */ + var transitions = new HashMap[Action, Set[State]] + + override def getNeverExploredActions: Set[Action] = neverExploredActions + override def getTransitions: Map[Action, Set[State]] = transitions + + def addTransition(a: Action, to: State): Unit = { + if (!transitions.contains(a)) { + transitions = transitions + (a -> HashSet(to)) + // TODO #4 This is not done in the legacy code: + neverExploredActions -= a + } else { + transitions = transitions + (a -> (transitions(a) + to)) + } + } + + /** + * 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 + * for each root element. + */ + override def equals(obj: Any): Boolean = { + if (obj.isInstanceOf[StateImpl]) { + val other = obj.asInstanceOf[StateImpl] + this.descriptors eq other.descriptors + } else { + super.equals(obj) + } + } + + override def hashCode(): Int = this.descriptors.hashCode() +} diff --git a/src/main/scala/de/retest/guistatemachine/dsl/Action.scala b/src/main/scala/de/retest/guistatemachine/dsl/Action.scala index f283b03..2378220 100644 --- a/src/main/scala/de/retest/guistatemachine/dsl/Action.scala +++ b/src/main/scala/de/retest/guistatemachine/dsl/Action.scala @@ -1,3 +1,3 @@ package de.retest.guistatemachine.dsl -abstract class Action \ No newline at end of file +trait Action \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/dsl/FinalState.scala b/src/main/scala/de/retest/guistatemachine/dsl/FinalState.scala index 119e2c2..b008214 100644 --- a/src/main/scala/de/retest/guistatemachine/dsl/FinalState.scala +++ b/src/main/scala/de/retest/guistatemachine/dsl/FinalState.scala @@ -1,6 +1,6 @@ package de.retest.guistatemachine.dsl /** - * There can be more than one end state. + * NFAs can have more than one final state. */ -abstract class FinalState extends State \ No newline at end of file +trait FinalState extends State \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/dsl/InitialState.scala b/src/main/scala/de/retest/guistatemachine/dsl/InitialState.scala index 232ea99..fdbb4b2 100644 --- a/src/main/scala/de/retest/guistatemachine/dsl/InitialState.scala +++ b/src/main/scala/de/retest/guistatemachine/dsl/InitialState.scala @@ -1,3 +1,6 @@ package de.retest.guistatemachine.dsl -abstract class InitialState extends State \ No newline at end of file +/** + * NFAs have only one initial state. + */ +trait InitialState extends State \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/dsl/State.scala b/src/main/scala/de/retest/guistatemachine/dsl/State.scala index 7fef60b..ecd7763 100644 --- a/src/main/scala/de/retest/guistatemachine/dsl/State.scala +++ b/src/main/scala/de/retest/guistatemachine/dsl/State.scala @@ -2,17 +2,26 @@ package de.retest.guistatemachine.dsl import scala.collection.mutable.ListBuffer -abstract class State { +trait State { + /** + * The previous state has to be stored for the DSL only to reach the initial state. + */ private[dsl] var previous: State = null - // TODO Use a set? + // TODO Use a set instead of a list buffer? Actually it is a multi map with the action as key and multiple possible states as values. private[dsl] var transitions: ListBuffer[Transition] = ListBuffer.empty[Transition] + def getTransitions: Seq[Transition] = transitions + + /** + * Goes back to the initial state from the current state and returns it. + */ def getInitial: InitialState = { def getFirst: State = if (previous eq null) this else previous.getInitial getFirst.asInstanceOf[InitialState] } + // TODO Rename to -> def -(a: Action): Transition = { val t = Transition(this, a) transitions += t diff --git a/src/main/scala/de/retest/guistatemachine/dsl/StateMachine.scala b/src/main/scala/de/retest/guistatemachine/dsl/StateMachine.scala index 070d68d..398192b 100644 --- a/src/main/scala/de/retest/guistatemachine/dsl/StateMachine.scala +++ b/src/main/scala/de/retest/guistatemachine/dsl/StateMachine.scala @@ -8,7 +8,10 @@ import scala.collection.immutable.HashSet * TODO NFAs can have multiple initial states. Do we really need this? * TODO Make the constructor private. */ -final case class StateMachine(initial: InitialState, var previous: StateMachine) { +case class StateMachine(initial: InitialState, var previous: StateMachine) { + + def getInitial: InitialState = initial + /** * Appends another state machine. */ diff --git a/src/main/scala/de/retest/guistatemachine/dsl/StateMachines.scala b/src/main/scala/de/retest/guistatemachine/dsl/StateMachines.scala index fe52190..ab5bf16 100644 --- a/src/main/scala/de/retest/guistatemachine/dsl/StateMachines.scala +++ b/src/main/scala/de/retest/guistatemachine/dsl/StateMachines.scala @@ -5,7 +5,7 @@ import scala.collection.immutable.HashMap object StateMachines { def apply(f: => StateMachine): Seq[StateMachine] = { def constructSeq(s: StateMachine, seq: Seq[StateMachine]): Seq[StateMachine] = - if (s.previous ne null) constructSeq(s.previous, seq ++ Seq(s)) else seq + if (s.previous ne null) constructSeq(s.previous, Seq(s) ++ seq) else Seq(s) ++ seq constructSeq(f, Seq()) } diff --git a/src/main/scala/de/retest/guistatemachine/dsl/Transition.scala b/src/main/scala/de/retest/guistatemachine/dsl/Transition.scala index 769b598..cb36d66 100644 --- a/src/main/scala/de/retest/guistatemachine/dsl/Transition.scala +++ b/src/main/scala/de/retest/guistatemachine/dsl/Transition.scala @@ -3,6 +3,10 @@ package de.retest.guistatemachine.dsl // TODO Make constructor private pls case class Transition(from: State, a: Action, var to: State = null) { + def getAction: Action = a + def getTo: State = to + + // TODO Rename to -> def -(to: State): State = { to.previous = from this.to = to diff --git a/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala b/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala new file mode 100644 index 0000000..8cba7fc --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/rest/JsonFormatForIdMap.scala @@ -0,0 +1,30 @@ +package de.retest.guistatemachine.rest + +import de.retest.guistatemachine.model.Map +import de.retest.guistatemachine.model.Id +import spray.json.JsValue +import spray.json.JsonFormat +import spray.json.RootJsonFormat + +/** + * Transforms a [[de.retest.guistatemachine.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 [[de.retest.guistatemachine.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[Map[T]] { + + override def write(obj: Map[T]): JsValue = + jsonFormat0.write(obj.values.map { field => + (field._1.id.toString -> field._2) + }) + + override def read(json: JsValue): Map[T] = { + val map = jsonFormat0.read(json) + new Map[T](map.map { x => + (Id(x._1.toLong) -> x._2) + }) + } +} diff --git a/src/main/scala/de/retest/guistatemachine/RestService.scala b/src/main/scala/de/retest/guistatemachine/rest/RestService.scala similarity index 99% rename from src/main/scala/de/retest/guistatemachine/RestService.scala rename to src/main/scala/de/retest/guistatemachine/rest/RestService.scala index bb12855..c81b3a4 100644 --- a/src/main/scala/de/retest/guistatemachine/RestService.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/RestService.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine +package de.retest.guistatemachine.rest import akka.actor.ActorSystem import akka.http.scaladsl.Http diff --git a/src/main/scala/de/retest/guistatemachine/WebServer.scala b/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala similarity index 92% rename from src/main/scala/de/retest/guistatemachine/WebServer.scala rename to src/main/scala/de/retest/guistatemachine/rest/WebServer.scala index 22b2681..582cfb4 100644 --- a/src/main/scala/de/retest/guistatemachine/WebServer.scala +++ b/src/main/scala/de/retest/guistatemachine/rest/WebServer.scala @@ -1,15 +1,14 @@ -package de.retest.guistatemachine +package de.retest.guistatemachine.rest import scala.io.StdIn 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 scopt.OptionParser -case class Config(maxtime: Long = -1) - object WebServer extends App with RestService { final val HOST = "localhost" final val PORT = 8888 @@ -19,6 +18,8 @@ object WebServer extends App with RestService { // needed for the future flatMap/onComplete in the end implicit val executionContext = system.dispatcher + case class Config(maxtime: Long = -1) + val parser = new OptionParser[Config]("scopt") { head("gui-state-machine-api", "1.0") diff --git a/src/test/scala/de/retest/guistatemachine/api/AbstractApiSpec.scala b/src/test/scala/de/retest/guistatemachine/api/AbstractApiSpec.scala new file mode 100644 index 0000000..66533d3 --- /dev/null +++ b/src/test/scala/de/retest/guistatemachine/api/AbstractApiSpec.scala @@ -0,0 +1,40 @@ +package de.retest.guistatemachine.api + +import java.util.Collections + +import de.retest.ui.descriptors._ +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 { + + /** + * The standard constructor of RootElement leads to an exception. + * Hence, we have to use the constructor with arguments. + */ + class MockableRootElement + extends RootElement( + "retestId", + new IdentifyingAttributes(Collections.emptyList()), + new Attributes(), + new Screenshot("prefix", Array(1, 2, 3), Screenshot.ImageType.PNG), + Collections.emptyList(), + "screen0", + 0, + "My Window" + ) + + /** + * The identifying attributes and the contained components specify the equality but we mock everything for our tests. + * @return A new root element which is equal to itself but not to any other root element. + */ + def getRootElement(): RootElement = { + val r = mock[MockableRootElement] + (r.equals _).expects().returning(false) + (r.equals _).expects(r).returning(true) + r + } + + def getAction(): org.openqa.selenium.interactions.Action = mock[org.openqa.selenium.interactions.Action] +} diff --git a/src/test/scala/de/retest/guistatemachine/api/DescriptorsSpec.scala b/src/test/scala/de/retest/guistatemachine/api/DescriptorsSpec.scala new file mode 100644 index 0000000..4082d00 --- /dev/null +++ b/src/test/scala/de/retest/guistatemachine/api/DescriptorsSpec.scala @@ -0,0 +1,19 @@ +package de.retest.guistatemachine.api +import scala.collection.immutable.HashSet + +class DescriptorsSpec extends AbstractApiSpec { + + "Descriptor" should { + "not equal" in { + val d0 = Descriptors(HashSet(getRootElement())) + val d1 = Descriptors(HashSet(getRootElement())) + d0.equals(d1) shouldEqual false + } + + "equal" in { + val d0 = Descriptors(HashSet(getRootElement())) + val d1 = Descriptors(HashSet(getRootElement())) + d0.equals(d1) shouldEqual true + } + } +} diff --git a/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala b/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala new file mode 100644 index 0000000..9af8cbc --- /dev/null +++ b/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala @@ -0,0 +1,31 @@ +package de.retest.guistatemachine.api.impl + +import de.retest.guistatemachine.api.{AbstractApiSpec, Action, Descriptors} + +class GuiStateMachineImplSpec extends AbstractApiSpec { + + val rootElement0Mock = getRootElement() + val rootElement1Mock = getRootElement() + val action0Mock = getAction() + val action1Mock = getAction() + + "GuiStateMachine" should { + "get an initial state" in { + val initial = GuiStateMachineImpl.getState(getDescriptors, getNeverExploredActions) + initial.getNeverExploredActions.size shouldEqual 2 + initial.getTransitions.size shouldEqual 0 + } + + "add a transition" in { + val initial = GuiStateMachineImpl.getState(getDescriptors, getNeverExploredActions) + val s = GuiStateMachineImpl.executeAction(initial, Action(action0Mock), Descriptors(Set(rootElement0Mock)), getNeverExploredActions) + initial.getNeverExploredActions.size shouldEqual 1 + initial.getTransitions.size shouldEqual 1 + s.getNeverExploredActions.size shouldEqual 2 + s.getTransitions.size shouldEqual 0 + } + } + + def getDescriptors: Descriptors = Descriptors(Set(rootElement0Mock, rootElement1Mock)) + def getNeverExploredActions: Set[Action] = Set(Action(action0Mock), Action(action1Mock)) +} diff --git a/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala b/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala new file mode 100644 index 0000000..a28babc --- /dev/null +++ b/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala @@ -0,0 +1,24 @@ +package de.retest.guistatemachine.api.impl +import de.retest.guistatemachine.api.{AbstractApiSpec, Action, Descriptors} + +import scala.collection.immutable.HashSet + +class StateImplSpec extends AbstractApiSpec { + + val action0Mock = getAction() + val action1Mock = getAction() + + "StateImpl" should { + "not equal" in { + val s0 = new StateImpl(Descriptors(HashSet(getRootElement())), HashSet(Action(action0Mock))) + val s1 = new StateImpl(Descriptors(HashSet(getRootElement())), HashSet(Action(action1Mock))) + s0.equals(s1) shouldEqual false + } + + "equal" in { + val s0 = new StateImpl(Descriptors(HashSet(getRootElement())), HashSet(Action(action0Mock))) + val s1 = new StateImpl(Descriptors(HashSet(getRootElement())), HashSet(Action(action1Mock))) + s0.equals(s1) shouldEqual true + } + } +} diff --git a/src/test/scala/de/retest/guistatemachine/dsl/StateMachinesSpec.scala b/src/test/scala/de/retest/guistatemachine/dsl/StateMachinesSpec.scala index dd0c5a1..142577e 100644 --- a/src/test/scala/de/retest/guistatemachine/dsl/StateMachinesSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/dsl/StateMachinesSpec.scala @@ -10,27 +10,48 @@ class StateMachinesSpec extends WordSpec with Matchers { "StateMachines" should { "be constructed as NFAs from objects" in { - case object Start extends InitialState - case object S0 extends State - case object S1 extends State - case object End extends FinalState + case object Start0 extends InitialState + case object S0_0 extends State + case object S0_1 extends State + case object End0 extends FinalState + + case object Start1 extends InitialState + case object S1_0 extends State + case object S1_1 extends State + case object End1 extends FinalState + case object EnterText extends Action case object PressExitButton extends Action - StateMachines { + val r = StateMachines { StateMachine { - Start - EnterText - S0 - Start - EnterText - S1 - S0 - PressExitButton - End - S1 - PressExitButton - End + Start0 - EnterText - S0_0 + Start0 - EnterText - S0_1 + S0_0 - PressExitButton - End0 + S0_1 - PressExitButton - End0 } ~ StateMachine { - Start - EnterText - S0 - Start - EnterText - S1 - S0 - PressExitButton - End - S1 - PressExitButton - End + Start1 - EnterText - S1_0 + Start1 - EnterText - S1_1 + S1_0 - PressExitButton - End1 + S1_1 - PressExitButton - End1 } } + + r.size shouldEqual 2 + val stateMachine0 = r(0) + + val initial0 = stateMachine0.getInitial + initial0 should be theSameInstanceAs Start0 + initial0.getTransitions.size shouldEqual 2 + + val initial0Trans0 = initial0.getTransitions(0) + initial0Trans0.getAction should be theSameInstanceAs EnterText + initial0Trans0.getTo should be theSameInstanceAs S0_0 + + val initial0Trans1 = initial0.getTransitions(1) + initial0Trans1.getAction should be theSameInstanceAs EnterText + initial0Trans1.getTo should be theSameInstanceAs S0_1 } } } \ No newline at end of file diff --git a/src/test/scala/de/retest/guistatemachine/JsonFormatForIdMapSpec.scala b/src/test/scala/de/retest/guistatemachine/rest/JsonFormatForIdMapSpec.scala similarity index 98% rename from src/test/scala/de/retest/guistatemachine/JsonFormatForIdMapSpec.scala rename to src/test/scala/de/retest/guistatemachine/rest/JsonFormatForIdMapSpec.scala index b462f9f..f6ac8d8 100644 --- a/src/test/scala/de/retest/guistatemachine/JsonFormatForIdMapSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/rest/JsonFormatForIdMapSpec.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine +package de.retest.guistatemachine.rest import org.scalatest.WordSpec import org.scalatest.Matchers diff --git a/src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala b/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala similarity index 99% rename from src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala rename to src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala index 0c05d34..fcce136 100644 --- a/src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/rest/RestServiceSpec.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine +package de.retest.guistatemachine.rest import org.scalatest.{ Matchers, WordSpec } import akka.http.scaladsl.model.StatusCodes diff --git a/src/test/scala/de/retest/guistatemachine/WebServerSpec.scala b/src/test/scala/de/retest/guistatemachine/rest/WebServerSpec.scala similarity index 85% rename from src/test/scala/de/retest/guistatemachine/WebServerSpec.scala rename to src/test/scala/de/retest/guistatemachine/rest/WebServerSpec.scala index 1d1b8e1..34b5af5 100644 --- a/src/test/scala/de/retest/guistatemachine/WebServerSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/rest/WebServerSpec.scala @@ -1,7 +1,7 @@ -package de.retest.guistatemachine +package de.retest.guistatemachine.rest -import org.scalatest.WordSpec import org.scalatest.Matchers +import org.scalatest.WordSpec class WebServerSpec extends WordSpec with Matchers {