Skip to content
This repository has been archived by the owner on Mar 12, 2020. It is now read-only.

Commit

Permalink
Add API package and refactor code #3 #4
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
tdauth committed Nov 5, 2018
1 parent 1d8f740 commit d729a51
Show file tree
Hide file tree
Showing 30 changed files with 369 additions and 63 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
/bin/
/target/
/.classpath
/.settings
/.idea
/bin
/target
/project/project
/project/target
1 change: 1 addition & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
maxColumn = 160
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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
Expand All @@ -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/).
Expand Down
14 changes: 11 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
mainClass in (Compile, packageBin) := Some("de.retest.guistatemachine.rest.WebServer")
1 change: 1 addition & 0 deletions project/scalafmt.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.5.1")
26 changes: 0 additions & 26 deletions src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala

This file was deleted.

6 changes: 6 additions & 0 deletions src/main/scala/de/retest/guistatemachine/api/Action.scala
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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])
29 changes: 29 additions & 0 deletions src/main/scala/de/retest/guistatemachine/api/GuiStateMachine.scala
Original file line number Diff line number Diff line change
@@ -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
}
15 changes: 15 additions & 0 deletions src/main/scala/de/retest/guistatemachine/api/State.scala
Original file line number Diff line number Diff line change
@@ -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]]
}
Original file line number Diff line number Diff line change
@@ -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
}
}
42 changes: 42 additions & 0 deletions src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala
Original file line number Diff line number Diff line change
@@ -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()
}
2 changes: 1 addition & 1 deletion src/main/scala/de/retest/guistatemachine/dsl/Action.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package de.retest.guistatemachine.dsl

abstract class Action
trait Action
4 changes: 2 additions & 2 deletions src/main/scala/de/retest/guistatemachine/dsl/FinalState.scala
Original file line number Diff line number Diff line change
@@ -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
trait FinalState extends State
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package de.retest.guistatemachine.dsl

abstract class InitialState extends State
/**
* NFAs have only one initial state.
*/
trait InitialState extends State
13 changes: 11 additions & 2 deletions src/main/scala/de/retest/guistatemachine/dsl/State.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/de/retest/guistatemachine/dsl/Transition.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine
package de.retest.guistatemachine.rest

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
Expand Down
Loading

0 comments on commit d729a51

Please sign in to comment.