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

Commit

Permalink
Add initial DSL and model support for state machines #
Browse files Browse the repository at this point in the history
  • Loading branch information
tdauth committed Oct 30, 2018
1 parent 337fed7 commit 1d8f740
Show file tree
Hide file tree
Showing 22 changed files with 294 additions and 14 deletions.
65 changes: 53 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,27 +32,68 @@ Whenever an unknown state is replaced by a newly discovered state, the NFA has t
**At the moment, the following definitions are incomplete and must be adapted to the actual implementation which calls this service.**

### Test Suite
A test suite is a set of test cases.
A set of test cases.

### Test Case
A test case is a sequence of UI actions.
A sequence of UI actions.

### UI Action
A UI action is an action which can be triggert by the user via the GUI.
An action which can be triggered by the user via the GUI.

### UI Path
A UI path is a sequence of states with transitions from one state to another.
A sequence of states with transitions from one state to another.
Each transition is a UI action.

### State
A state is defined by the set of all visible and interactable windows together with their enabled widgets.

## DSL
There is a DSL to construct an NFA with GUI actions manually.
The package [dsl](./src/main/scala/de/retest/guistatemachine/dsl/).

The following example shows how to construct an NFA in Scala:
```scala
case object Start extends InitialState
case object S0 extends State
case object S1 extends State
case object End extends FinalState
case object EnterText extends Action
case object PressExitButton extends Action

StateMachines {
StateMachine {
Start - EnterText - S0
Start - EnterText - S1
S0 - PressExitButton - End
S1 - PressExitButton - End
}
}
```

## REST API
Some suggestions on how the REST API could look like:

* `/applications` GET queries all registered GUI applications
* `/create-application` POST registers a new GUI application
* `/application/<long>` GET queries a registered GUI application
* `/application/<long>/test-suites` GET queries all test suites for an existing GUI application
* `/application/<long>/create-test-suite` POST registers a new test suite for an existing GUI application
* `/application/<long>/test-suite/<long>` GET queries a registered test suite for an existing GUI application
Some suggestions how the REST API for the state machine could look like:
* `/state-machines` GET queries all existing state machines.
* `/create-state-machine` POST creates a new state machine.
* `/state-machine/<long>` GET queries an existing state machine.
* `/state-machine/<long>/states` GET queries all existing states of the state machine.
* `/state-machine/<long>/state/<long>` GET queries a specific state of the state machine which contains transitions.
* `/state-machine/<long>/state/<long>/transitions` GET queries all transitions of a specific state.
* `/state-machine/<long>/state/<long>/transition/<long>` GET queries a specific transition of a specific state.
* `/state-machine/<long>/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?

Some suggestions on how the test representation REST API could look like (not necessarily required):

* `/applications` GET queries all existing GUI applications.
* `/create-application` POST creates a new GUI application.
* `/application/<long>` GET queries an existing GUI application.
* `/application/<long>` DELETE deletes an existing GUI application and all of its test suites etc.
* `/application/<long>/test-suites` GET queries all test suites for an existing GUI application.
* `/application/<long>/create-test-suite` POST creates a new test suite for an existing GUI application.
* `/application/<long>/test-suite/<long>` GET queries an existing test suite for an existing GUI application.
* `/application/<long>/test-suite/<long>` DELETE deletes an existing test suite for an existing GUI application.

## NFA Frameworks
This list contains frameworks for Scala which support the representation of an NFA:
* Akka FSM (FSM for actors): <https://doc.akka.io/docs/akka/current/fsm.html>
* Neo4J: <https://neo4j.com/>
* Gremlin-Scala: <https://github.com/mpollmeier/gremlin-scala>
29 changes: 27 additions & 2 deletions src/main/scala/de/retest/guistatemachine/RestService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,40 @@ import de.retest.guistatemachine.model.TestSuite
import de.retest.guistatemachine.model.TestSuites
import de.retest.guistatemachine.model.Id
import de.retest.guistatemachine.persistence.Persistence
import de.retest.guistatemachine.model.StateMachine
import de.retest.guistatemachine.model.Transition
import de.retest.guistatemachine.model.Transitions
import de.retest.guistatemachine.model.Action
import de.retest.guistatemachine.model.Actions
import de.retest.guistatemachine.model.StateMachines
import de.retest.guistatemachine.model.State
import de.retest.guistatemachine.model.States

trait RestService {
implicit val system: ActorSystem
implicit val materializer: ActorMaterializer

// 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)

implicit val testSuiteFormat = jsonFormat0(TestSuite)
implicit val hashMapFormatTestSuites = new JsonFormatForIdMap[TestSuite]
implicit val idMapFormatTestSuites = new JsonFormatForIdMap[TestSuite]
implicit val testSuitesFormat = jsonFormat1(TestSuites)
implicit val applicationFormat = jsonFormat1(GuiApplication)
implicit val hashMapFormatApplications = new JsonFormatForIdMap[GuiApplication]
implicit val idMapFormatApplications = new JsonFormatForIdMap[GuiApplication]
implicit val applicationsFormat = jsonFormat1(GuiApplications)

/**
Expand All @@ -43,6 +65,9 @@ trait RestService {
pathSingleSlash {
complete("GUI State Machine API")
} ~
path("state-machines") {
complete(persistence.getStateMachines())
} ~
path("applications") {
complete(persistence.getApplications())
} ~
Expand Down
1 change: 1 addition & 0 deletions src/main/scala/de/retest/guistatemachine/WebServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,6 @@ object WebServer extends App with RestService {
bindingFuture
.flatMap(_.unbind()) // trigger unbinding from the port
.onComplete(_ => system.terminate()) // and shutdown when done
case None =>
}
}
3 changes: 3 additions & 0 deletions src/main/scala/de/retest/guistatemachine/dsl/Action.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.retest.guistatemachine.dsl

abstract class Action
6 changes: 6 additions & 0 deletions src/main/scala/de/retest/guistatemachine/dsl/FinalState.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.retest.guistatemachine.dsl

/**
* There can be more than one end state.
*/
abstract class FinalState extends State
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.retest.guistatemachine.dsl

abstract class InitialState extends State
21 changes: 21 additions & 0 deletions src/main/scala/de/retest/guistatemachine/dsl/State.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package de.retest.guistatemachine.dsl

import scala.collection.mutable.ListBuffer

abstract class State {
private[dsl] var previous: State = null
// TODO Use a set?
private[dsl] var transitions: ListBuffer[Transition] = ListBuffer.empty[Transition]

def getInitial: InitialState = {
def getFirst: State = if (previous eq null) this else previous.getInitial

getFirst.asInstanceOf[InitialState]
}

def -(a: Action): Transition = {
val t = Transition(this, a)
transitions += t
t
}
}
47 changes: 47 additions & 0 deletions src/main/scala/de/retest/guistatemachine/dsl/StateMachine.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package de.retest.guistatemachine.dsl

import scala.collection.immutable.HashSet

/**
* NFA:
* 5–Tupel (Z, Σ, δ, z0, E)
* 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) {
/**
* Appends another state machine.
*/
def ~(s: StateMachine): StateMachine = {
s.previous = this
s
}

/**
* All states.
*/
//def Z(): Set[State]
/**
* Input alphabet.
*/
//def Σ(): Set[Action]
/**
* Partially defined function which returns the next state.
*/
//def δ(s: State, a: Action): State
/**
* Initial state.
* TODO NFAs can have multiple initial states. Do we really need this?
*/
//def z0: InitialState = initial

/**
* All final states.
*/
//def E: Set[FinalState]
}

object StateMachine {
// TODO f should return a FinalState since all state machines have to end with one
def apply(f: => State): StateMachine = StateMachine(f.getInitial, null)
}
25 changes: 25 additions & 0 deletions src/main/scala/de/retest/guistatemachine/dsl/StateMachines.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.retest.guistatemachine.dsl

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

constructSeq(f, Seq())
}

def toModel(s: Seq[StateMachine]): de.retest.guistatemachine.model.StateMachines = {
val hashmap = new HashMap[de.retest.guistatemachine.model.Id, de.retest.guistatemachine.model.StateMachine]()

// TODO Implement conversion from the DSL to the model.
/*
s.foreach(x => {
de.retest.guistatemachine.model.StateMachine
})
*/

de.retest.guistatemachine.model.StateMachines(de.retest.guistatemachine.model.Map[de.retest.guistatemachine.model.StateMachine](hashmap))
}
}
11 changes: 11 additions & 0 deletions src/main/scala/de/retest/guistatemachine/dsl/Transition.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package de.retest.guistatemachine.dsl

// TODO Make constructor private pls
case class Transition(from: State, a: Action, var to: State = null) {

def -(to: State): State = {
to.previous = from
this.to = to
to
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.retest.guistatemachine.dsl

/**
* All actions can be executed for an unknown state.
*/
case object UnknownState extends State
3 changes: 3 additions & 0 deletions src/main/scala/de/retest/guistatemachine/model/Action.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.retest.guistatemachine.model

case class Action()
3 changes: 3 additions & 0 deletions src/main/scala/de/retest/guistatemachine/model/Actions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.retest.guistatemachine.model

case class Actions(actions: Map[Action])
5 changes: 5 additions & 0 deletions src/main/scala/de/retest/guistatemachine/model/State.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.retest.guistatemachine.model

import scala.collection.immutable.HashMap

final case class State(transitions: Transitions)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.retest.guistatemachine.model

/**
* State machine which represents a GUI test.
*/
final case class StateMachine(states: States, actions: Actions)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.retest.guistatemachine.model

final case class StateMachines(stateMachines: Map[StateMachine])
3 changes: 3 additions & 0 deletions src/main/scala/de/retest/guistatemachine/model/States.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.retest.guistatemachine.model

case class States(states: Map[State])
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.retest.guistatemachine.model

final case class Transition(to : Id, action : Id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package de.retest.guistatemachine.model

final case class Transitions(transitions: Map[Transition])
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ import de.retest.guistatemachine.model.Id
import de.retest.guistatemachine.model.TestSuite
import de.retest.guistatemachine.model.TestSuites
import de.retest.guistatemachine.model.Map
import de.retest.guistatemachine.model.StateMachines
import de.retest.guistatemachine.model.StateMachine

/**
* Allows concurrent access to the persistence of the resources.
* The actual persistence layer is hidden by this class.
*/
class Persistence {
// database
private val stateMachines = StateMachines(Map(new HashMap[Id, StateMachine]))
private val guiApplications = GuiApplications(Map(new HashMap[Id, GuiApplication]))

def getStateMachines(): StateMachines = stateMachines

def getApplications(): GuiApplications = guiApplications

def addApplication(): Id = {
Expand Down Expand Up @@ -95,6 +100,7 @@ class Persistence {
}
}
}
case None => false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package de.retest.guistatemachine.dsl

import org.scalatest.WordSpec
import org.scalatest.Matchers

/**
* Tests the construction of state machines with the DSL.
*/
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 EnterText extends Action
case object PressExitButton extends Action

StateMachines {
StateMachine {
Start - EnterText - S0
Start - EnterText - S1
S0 - PressExitButton - End
S1 - PressExitButton - End
} ~
StateMachine {
Start - EnterText - S0
Start - EnterText - S1
S0 - PressExitButton - End
S1 - PressExitButton - End
}
}
}
}
}
Loading

0 comments on commit 1d8f740

Please sign in to comment.