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

Commit

Permalink
Refactor to separate serialization and add default GuiStateMachineApi #…
Browse files Browse the repository at this point in the history
…17

object GuiStateMachineSerializer allows creating serializers for state
machines.
object GuiStateMachineApi can be used to get the default implementation.
  • Loading branch information
tdauth committed Mar 19, 2019
1 parent ddf959f commit 0ca5a05
Show file tree
Hide file tree
Showing 20 changed files with 417 additions and 299 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1 +1 @@
maxColumn = 160
maxColumn = 160
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ script:
- sbt clean coverage test coverageReport scalastyle doc assembly

after_success:
- bash <(curl -s https://codecov.io/bash)
- bash <(curl -s https://codecov.io/bash)
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# GUI State Machine API

API for the creation and modification of nondeterministic finite automaton for the automatic generation of GUI tests with the help of a genetic algorithm.
API for the creation and modification of incomplete state machines which represent the exploration of a GUI application.
The states represent the GUI elements and the transitions represent the GUI actions.

This is a small code example of creating a new state machine, adding two states connected with a transition and saving the state machine:
```scala
import de.retest.guistatemachine.api.impl.GuiStateMachineApiImpl
import de.retest.guistatemachine.api.GuiStateMachineApi
import de.retest.guistatemachine.api.GuiStateMachineSerializer
import de.retest.recheck.ui.descriptors.SutState
import de.retest.surili.commons.actions.NavigateToAction

val guiStateMachineApi = new GuiStateMachineApiImpl
val stateMachineId = guiStateMachineApi.createStateMachine()
val stateMachine = guiStateMachineApi.getStateMachine(stateMachineId).get
val stateMachineId = GuiStateMachineApi().createStateMachine()
val stateMachine = GuiStateMachineApi().getStateMachine(stateMachineId).get
val currentState = new SutState(currentDescriptors)
val action = new NavigateToAction("http://google.com")
val nextState = new SutState(nextDescriptors)
stateMachine.executeAction(currentState, action, nextState)
stateMachine.saveGML("mystatemachine.gml")
stateMachine.save("mystatemachine.ser")

GuiStateMachineSerializer.javaObjectStream(stateMachine).save("mystatemachine.ser")
GuiStateMachineSerializer.gml(stateMachine).save("mystatemachine.gml")
```

State machines can be saved as and loaded from files using Java object serialization/deserialization.
Expand Down
26 changes: 5 additions & 21 deletions src/main/scala/de/retest/guistatemachine/api/GuiStateMachine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ trait GuiStateMachine {
* @return The current state which the transition of a leads to.
*/
def executeAction(from: State, a: Action, to: State): State
def executeAction(fromSutState: SutState, a: Action, toSutState: SutState): State = executeAction(getState(fromSutState), a, getState(toSutState))
def executeAction(fromSutState: SutState, a: Action, toSutState: SutState): State =
executeAction(getState(fromSutState), a, getState(toSutState))

def getAllStates: Map[SutState, State]

Expand All @@ -55,26 +56,9 @@ trait GuiStateMachine {
def clear(): Unit

/**
* Stores the state machine on the disk.
* Persistence can be useful when the state machines become quite big and the generation/modification is interrupted
* and continued later.
* Clears the current states and assigns them from another state machine.
*
* @param filePath The file which the state machine is stored into.
* @param other The other state machine.
*/
def save(filePath: String): Unit

/**
* Clears the state machine and loads it from the disk.
*
* @param filePath The file which the state machine is loaded from.
*/
def load(filePath: String): Unit

/**
* Converts the state machines into GML which can be read by editors like yED.
*
* @param filePath The file which the GML data is stored into.
* @throws RuntimeException If a vertex or edge cannot be added, this exception is thrown.
*/
def saveGML(filePath: String): Unit
def assignFrom(other: GuiStateMachine): Unit
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package de.retest.guistatemachine.api
import de.retest.guistatemachine.api.impl.GuiStateMachineApiImpl

/**
* This API allows the creation, modification and deletion of state machines ([[GuiStateMachine]]) which are created
* during test generations with the help of Genetic Algorithms.
* To store the state machines permanently, you have to call [[GuiStateMachineApi.save]] manually.
* Otherwise, they will only be stored in the memory.
* [[GuiStateMachineApi.load]] allows loading state machines from a file.
*/
trait GuiStateMachineApi {

Expand Down Expand Up @@ -36,20 +34,13 @@ trait GuiStateMachineApi {
* Clears all state machines.
*/
def clear(): Unit
}

/**
* 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.
*
* @param filePath The file which the state machines are stored into.
*/
def save(filePath: String): Unit
object GuiStateMachineApi {
private val impl = new GuiStateMachineApiImpl

/**
* Clears all current state machines and loads all state machines from the disk.
*
* @param filePath The file which the state machines are loaded from.
* @return The standard implementaiton of the API.
*/
def load(filePath: String): Unit
def apply(): GuiStateMachineApi = impl
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package de.retest.guistatemachine.api

import de.retest.guistatemachine.api.impl.serialization.{GuiStateMachinGMLSerializer, GuiStateMachineJavaObjectStreamSerializer}

trait GuiStateMachineSerializer {
def save(filePath: String)
def load(filePath: String)
}

object GuiStateMachineSerializer {
def javaObjectStream(guiStateMachine: GuiStateMachine): GuiStateMachineSerializer = GuiStateMachineJavaObjectStreamSerializer(guiStateMachine)
def gml(guiStateMachine: GuiStateMachine): GuiStateMachineSerializer = GuiStateMachinGMLSerializer(guiStateMachine)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package de.retest.guistatemachine.api.impl

import java.io.{FileInputStream, FileOutputStream, ObjectInputStream, ObjectOutputStream}

import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi, Id}

/**
* Thread-safe implementation of the API. It is thread-safe because it uses `IdMap`.
*/
class GuiStateMachineApiImpl extends GuiStateMachineApi {
private var stateMachines = IdMap[GuiStateMachine]()
private val stateMachines = IdMap[GuiStateMachine]()

override def createStateMachine(): Id = stateMachines.addNewElement(new GuiStateMachineImpl)

Expand All @@ -14,19 +15,4 @@ class GuiStateMachineApiImpl extends GuiStateMachineApi {
override def getStateMachine(id: Id): Option[GuiStateMachine] = stateMachines.getElement(id)

override def clear(): Unit = stateMachines.clear()

override def save(filePath: String): Unit = {
val oos = new ObjectOutputStream(new FileOutputStream(filePath))
oos.writeObject(stateMachines) // TODO #15 Do we need to make a copy before to make it threadsafe?
oos.close()
}

// TODO #15 Make thread safe?
override def load(filePath: String): Unit = {
clear()
val ois = new ObjectInputStream(new FileInputStream(filePath))
val readStateMachines = ois.readObject.asInstanceOf[IdMap[GuiStateMachine]]
ois.close()
stateMachines = readStateMachines
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
package de.retest.guistatemachine.api.impl

import java.io._

import com.github.systemdir.gml.YedGmlWriter
import com.typesafe.scalalogging.Logger
import de.retest.guistatemachine.api.{GuiStateMachine, State}
import de.retest.recheck.ui.descriptors.SutState
import de.retest.surili.commons.actions.Action
import org.jgrapht.graph.DirectedPseudograph

import scala.collection.immutable.{HashMap, HashSet}

/**
* Thread-safe implementation of a GUI state machine.
*/
@SerialVersionUID(1L)
class GuiStateMachineImpl extends GuiStateMachine with Serializable {
@transient private val logger = Logger[GuiStateMachineImpl]
// Make it accessible from the impl package for unit tests.
private var states = new HashMap[SutState, State]

/**
Expand All @@ -33,7 +31,7 @@ class GuiStateMachineImpl extends GuiStateMachine with Serializable {
states(sutState)
} else {
logger.info(s"Create new state from SUT state with hash code ${sutState.hashCode()}")
val s = new StateImpl(sutState)
val s = StateImpl(sutState)
states += (sutState -> s)
s
}
Expand Down Expand Up @@ -62,69 +60,11 @@ class GuiStateMachineImpl extends GuiStateMachine with Serializable {
actionExecutionTimes = new HashMap[Action, Int]
}

override def save(filePath: String): Unit = this.synchronized {
val oos = new ObjectOutputStream(new FileOutputStream(filePath))
oos.writeObject(this)
oos.close()
}

override def load(filePath: String): Unit = this.synchronized {
override def assignFrom(other: GuiStateMachine): Unit = this.synchronized {
clear()
val ois = new ObjectInputStream(new FileInputStream(filePath))
val readStateMachine = ois.readObject.asInstanceOf[GuiStateMachineImpl]
ois.close()
states = readStateMachine.states
allExploredActions = readStateMachine.allExploredActions
actionExecutionTimes = readStateMachine.actionExecutionTimes
}

type GraphType = DirectedPseudograph[SutState, GraphActionEdge]

override def saveGML(filePath: String): Unit = this.synchronized {
// get graph from user
val toDraw = getGraph()

// define the look and feel of the graph
val graphicsProvider = new GraphicsProvider

// get the gml writer
val writer =
new YedGmlWriter.Builder[SutState, GraphActionEdge, AnyRef](graphicsProvider, YedGmlWriter.PRINT_LABELS: _*)
.setEdgeLabelProvider(_.toString)
.setVertexLabelProvider(sutState => "%s - hash code: %d".format(sutState.toString, sutState.hashCode()))
.build

// write to file
val outputFile = new File(filePath)
val output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), "utf-8"))
try writer.export(output, toDraw)
finally if (output != null) output.close()
}

private def getGraph(): GraphType = {
val graph = new GraphType(classOf[GraphActionEdge])
val allStatesSorted = getAllStates.toSeq.sortWith(hashCodeComparisonOfTuples)
allStatesSorted.foreach { x =>
val vertex = x._1
if (!graph.addVertex(vertex)) throw new RuntimeException(s"Failed to add vertex $vertex")
}

allStatesSorted.foreach { x =>
val fromVertex = x._1
val allTransitionsSorted = x._2.getTransitions.toSeq.sortWith(hashCodeComparisonOfTuples)

allTransitionsSorted foreach { transition =>
val actionTransitions = transition._2
val action = transition._1
actionTransitions.to.foreach { toState =>
val toVertex = toState.getSutState
val edge = GraphActionEdge(fromVertex, toVertex, action)
if (!graph.addEdge(fromVertex, toVertex, edge)) throw new RuntimeException(s"Failed to add edge $edge")
}
}
}
graph
val otherStateMachine = other.asInstanceOf[GuiStateMachineImpl]
states = otherStateMachine.states
allExploredActions = otherStateMachine.allExploredActions
actionExecutionTimes = otherStateMachine.actionExecutionTimes
}

private def hashCodeComparisonOfTuples[A, B](a: (A, B), b: (A, B)) = a._1.hashCode().compareTo(b._2.hashCode()) < 0
}
11 changes: 9 additions & 2 deletions src/main/scala/de/retest/guistatemachine/api/impl/IdMap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import de.retest.guistatemachine.api.Id
import scala.collection.mutable.HashMap

/**
* This custom type allows storing values using [[Id]] as key.
* We cannot extend immutable maps in Scala, so we have to keep it as field.
* This custom type allows storing values using [[Id]] as key. We cannot extend immutable maps in Scala, so we have to
* keep it as field. The implementation is thread-safe.
*/
@SerialVersionUID(1L)
case class IdMap[T]() extends Serializable {
Expand Down Expand Up @@ -48,6 +48,13 @@ case class IdMap[T]() extends Serializable {
}

object IdMap {

/**
* Creates a new `IdMap` by a number of values.
* @param v The initial values of the `IdMap`.
* @tparam T The type of the values of the `IdMap`.
* @return A newly created `IdMap`.
*/
def apply[T](v: T*): IdMap[T] = {
val r = new IdMap[T]()
for (e <- v) { r.addNewElement(e) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import scala.collection.immutable.HashMap
case class StateImpl(sutState: SutState) extends State with Serializable {

/**
* TODO #4 Currently, there is no MultiMap trait for immutable maps in the Scala standard library.
* 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, ActionTransitions]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
package de.retest.guistatemachine.api.impl

package de.retest.guistatemachine.api.impl.serialization
import de.retest.recheck.ui.descriptors.SutState
import de.retest.surili.commons.actions.Action

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine.api.impl
package de.retest.guistatemachine.api.impl.serialization

import java.awt.Color

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package de.retest.guistatemachine.api.impl.serialization
import java.io.{BufferedWriter, File, FileOutputStream, OutputStreamWriter}

import com.github.systemdir.gml.YedGmlWriter
import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineSerializer}
import de.retest.recheck.ui.descriptors.SutState
import org.jgrapht.graph.DirectedPseudograph

case class GuiStateMachinGMLSerializer(guiStateMachine: GuiStateMachine) extends GuiStateMachineSerializer {

type GraphType = DirectedPseudograph[SutState, GraphActionEdge]

/**
* Converts the state machines into GML which can be read by editors like yED.
*
* @param filePath The file which the GML data is stored into.
* @throws RuntimeException If a vertex or edge cannot be added, this exception is thrown.
*/
override def save(filePath: String): Unit = {
// get graph from user
val toDraw = createGraph()

// define the look and feel of the graph
val graphicsProvider = new GraphicsProvider

// get the gml writer
val writer =
new YedGmlWriter.Builder[SutState, GraphActionEdge, AnyRef](graphicsProvider, YedGmlWriter.PRINT_LABELS: _*)
.setEdgeLabelProvider(_.toString)
.setVertexLabelProvider(sutState => "%s - hash code: %d".format(sutState.toString, sutState.hashCode()))
.build

// write to file
val outputFile = new File(filePath)
val output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), "utf-8"))
try { writer.export(output, toDraw) } finally { if (output != null) { output.close() } }
}

override def load(filePath: String): Unit = throw new RuntimeException("Loading GML is not supported.")

private def createGraph(): GraphType = {
val graph = new GraphType(classOf[GraphActionEdge])
val allStatesSorted = guiStateMachine.getAllStates.toSeq.sortWith(hashCodeComparisonOfTuples)
allStatesSorted.foreach { x =>
val vertex = x._1
if (!graph.addVertex(vertex)) { throw new RuntimeException(s"Failed to add vertex $vertex") }
}

allStatesSorted.foreach { x =>
val fromVertex = x._1
val allTransitionsSorted = x._2.getTransitions.toSeq.sortWith(hashCodeComparisonOfTuples)

allTransitionsSorted foreach { transition =>
val actionTransitions = transition._2
val action = transition._1
actionTransitions.to.foreach { toState =>
val toVertex = toState.getSutState
val edge = GraphActionEdge(fromVertex, toVertex, action)
if (!graph.addEdge(fromVertex, toVertex, edge)) { throw new RuntimeException(s"Failed to add edge $edge") }
}
}
}
graph
}

private def hashCodeComparisonOfTuples[A, B](a: (A, B), b: (A, B)) = a._1.hashCode().compareTo(b._2.hashCode()) < 0

}
Loading

0 comments on commit 0ca5a05

Please sign in to comment.