From 3c671394c8a4810ed922332bfbc3609b4415f20f Mon Sep 17 00:00:00 2001 From: Tamino Dauth Date: Tue, 26 Feb 2019 12:48:14 +0100 Subject: [PATCH] Initial concurrency support #15 Basic concurrency support with synchronized. --- README.md | 12 +++++- build.sbt | 2 +- .../de/retest/guistatemachine/api/State.scala | 2 - .../api/impl/GraphicsProvider.scala | 1 - .../api/impl/GuiStateMachineApiImpl.scala | 3 +- .../api/impl/GuiStateMachineImpl.scala | 43 +++++++++---------- .../guistatemachine/api/impl/IdMap.scala | 25 +++++------ .../guistatemachine/api/impl/StateImpl.scala | 6 +-- .../api/impl/GuiStateMachineImplSpec.scala | 2 +- .../api/impl/StateImplSpec.scala | 3 -- 10 files changed, 51 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 4b51e61..3719b6b 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,11 @@ stateMachine.saveGML("mystatemachine.gml") stateMachine.save("mystatemachine.ser") ``` -The state machine can be saved as [GML](https://en.wikipedia.org/wiki/Graph_Modelling_Language) file which can be visualized by editors like [yEd](https://www.yworks.com/products/yed). +State machines can be saved as on loaded from files. +Besides, they can be saved as [GML](https://en.wikipedia.org/wiki/Graph_Modelling_Language) files which can be visualized by editors like [yEd](https://www.yworks.com/products/yed). ## Automatic Build with TravisCI + [![Build Status](https://travis-ci.com/retest/gui-state-machine-api.svg?branch=master)](https://travis-ci.com/retest/gui-state-machine-api) [![Code Coverage](https://img.shields.io/codecov/c/github/retest/gui-state-machine-api/master.svg)](https://codecov.io/github/retest/gui-state-machine-api?branch=master) @@ -30,6 +32,7 @@ Define the Nexus password in the environment variable `TRAVIS_NEXUS_PW`. Otherwise, the build will fail! ## SBT Commands + * `sbt compile` to build the project manually. * `sbt assembly` to create a standalone JAR which includes all dependencies including the Scala libraries. The standalone JAR is generated as `target/scala-/gui-state-machine-api-assembly-.jar`. * `sbt eclipse` to generate a project for Eclipse. @@ -43,6 +46,7 @@ Otherwise, the build will fail! * `sbt publish` publishes the artifacts in ReTest's Nexus. Requires a `$HOME/.sbt/.credentials` file with the correct credentials. This command can be useful to publish SNAPSHOT versions. ## 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 represented by transitions. If an action has not been executed yet from a state, it leads to an unknown state. @@ -52,4 +56,8 @@ Whenever an unknown state is replaced by a newly discovered state, the NFA has t 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. \ No newline at end of file +After running the genetic algorithm, the state machine is then used to create a test suite. + +## Concurrency + +The creation and modification of state machines should be threadsafe. diff --git a/build.sbt b/build.sbt index f429de2..7661275 100644 --- a/build.sbt +++ b/build.sbt @@ -22,7 +22,7 @@ resolvers += "nexus-retest-maven-all" at " https://nexus.retest.org/repository/a libraryDependencies += "de.retest" % "surili-model" % "0.1.0-SNAPSHOT" % "provided" withSources () withJavadoc () libraryDependencies += "de.retest" % "retest-sut-api" % "3.2.0" % "provided" withSources () withJavadoc () -// Dependencies to write GraphML files for yEd: +// Dependencies to write GML files for yEd: libraryDependencies += "com.github.systemdir.gml" % "GMLWriterForYed" % "2.1.0" libraryDependencies += "org.jgrapht" % "jgrapht-core" % "1.0.1" diff --git a/src/main/scala/de/retest/guistatemachine/api/State.scala b/src/main/scala/de/retest/guistatemachine/api/State.scala index c1c7947..020bf97 100644 --- a/src/main/scala/de/retest/guistatemachine/api/State.scala +++ b/src/main/scala/de/retest/guistatemachine/api/State.scala @@ -3,8 +3,6 @@ package de.retest.guistatemachine.api import de.retest.surili.model.actions.Action import de.retest.ui.descriptors.SutState -import scala.util.Random - /** * A state should be identified by its corresponding `de.retest.ui.descriptors.SutState`. * It consists of actions which have not been explored yet and transitions to states which build up the state machine. diff --git a/src/main/scala/de/retest/guistatemachine/api/impl/GraphicsProvider.scala b/src/main/scala/de/retest/guistatemachine/api/impl/GraphicsProvider.scala index 5c7fd08..59610af 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/GraphicsProvider.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/GraphicsProvider.scala @@ -13,6 +13,5 @@ class GraphicsProvider extends YedGmlGraphicsProvider[SutState, GraphActionEdge, .setTargetArrow(EdgeGraphicDefinition.ArrowType.SHORT_ARROW) .setLineType(GraphicDefinition.LineType.DASHED) .build - // we have no groups in this example override def getGroupGraphics(group: AnyRef, groupElements: java.util.Set[SutState]): NodeGraphicDefinition = null } 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 188bca4..fc8833f 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImpl.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineApiImpl.scala @@ -17,10 +17,11 @@ class GuiStateMachineApiImpl extends GuiStateMachineApi { override def save(filePath: String): Unit = { val oos = new ObjectOutputStream(new FileOutputStream(filePath)) - oos.writeObject(stateMachines) + 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)) 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 51253c9..b12e063 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImpl.scala @@ -20,67 +20,67 @@ class GuiStateMachineImpl extends GuiStateMachine with Serializable { /** * The legacy code stored execution counters for every action. */ - var allExploredActions = new HashSet[Action] + private var allExploredActions = new HashSet[Action] /** * `actionExecutionCounter` from the legacy code. * Stores the total number of executions per action. */ - var actionExecutionTimes = new HashMap[Action, Int] + private var actionExecutionTimes = new HashMap[Action, Int] - override def getState(sutState: SutState): State = { + override def getState(sutState: SutState): State = this.synchronized { if (states.contains(sutState)) { states(sutState) } else { logger.info(s"Create new state from SUT state with hash code ${sutState.hashCode()}") val s = new StateImpl(sutState) - states = states + (sutState -> s) + states += (sutState -> s) s } } - override def executeAction(from: State, a: Action, to: State): State = { - allExploredActions = allExploredActions + a + override def executeAction(from: State, a: Action, to: State): State = this.synchronized { + allExploredActions += a val old = actionExecutionTimes.get(a) old match { - case Some(o) => actionExecutionTimes = actionExecutionTimes + (a -> (o + 1)) - case None => actionExecutionTimes = actionExecutionTimes + (a -> 1) + case Some(o) => actionExecutionTimes += (a -> (o + 1)) + case None => actionExecutionTimes += (a -> 1) } from.addTransition(a, to) to } - override def getAllStates: Map[SutState, State] = states + override def getAllStates: Map[SutState, State] = this.synchronized { states } - override def getAllExploredActions: Set[Action] = allExploredActions + override def getAllExploredActions: Set[Action] = this.synchronized { allExploredActions } - override def getActionExecutionTimes: Map[Action, Int] = actionExecutionTimes + override def getActionExecutionTimes: Map[Action, Int] = this.synchronized { actionExecutionTimes } - override def clear(): Unit = { - states = HashMap[SutState, State]() - allExploredActions = HashSet[Action]() - actionExecutionTimes = HashMap[Action, Int]() + override def clear(): Unit = this.synchronized { + states = new HashMap[SutState, State] + allExploredActions = new HashSet[Action] + actionExecutionTimes = new HashMap[Action, Int] } - override def save(filePath: String): Unit = { + 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 = { + override def load(filePath: String): Unit = this.synchronized { clear() val ois = new ObjectInputStream(new FileInputStream(filePath)) val readStateMachine = ois.readObject.asInstanceOf[GuiStateMachineImpl] ois.close() - this.states = readStateMachine.states - this.allExploredActions = readStateMachine.allExploredActions - this.actionExecutionTimes = readStateMachine.actionExecutionTimes + states = readStateMachine.states + allExploredActions = readStateMachine.allExploredActions + actionExecutionTimes = readStateMachine.actionExecutionTimes } type GraphType = DirectedPseudograph[SutState, GraphActionEdge] - override def saveGML(filePath: String): Unit = { + override def saveGML(filePath: String): Unit = this.synchronized { // get graph from user val toDraw = getGraph() @@ -99,7 +99,6 @@ class GuiStateMachineImpl extends GuiStateMachine with Serializable { 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 = { diff --git a/src/main/scala/de/retest/guistatemachine/api/impl/IdMap.scala b/src/main/scala/de/retest/guistatemachine/api/impl/IdMap.scala index f897d29..35e7fc5 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/IdMap.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/IdMap.scala @@ -2,7 +2,7 @@ package de.retest.guistatemachine.api.impl import de.retest.guistatemachine.api.Id -import scala.collection.immutable.HashMap +import scala.collection.mutable.HashMap /** * This custom type allows storing values using [[Id]] as key. @@ -12,34 +12,35 @@ import scala.collection.immutable.HashMap case class IdMap[T]() extends Serializable { private type HashMapType = HashMap[Id, T] - private var values = new HashMapType + private val values = new HashMapType - def addNewElement(v: T): Id = { - val generatedId = generateId - values = values + (generatedId -> v) + def addNewElement(v: T): Id = this.synchronized { + val generatedId = generateId() + values += (generatedId -> v) generatedId } - def removeElement(id: Id): Boolean = + def removeElement(id: Id): Boolean = this.synchronized { if (values.contains(id)) { - values = values - id + values -= id true } else { false } + } - def getElement(id: Id): Option[T] = values.get(id) + def getElement(id: Id): Option[T] = this.synchronized { values.get(id) } - def hasElement(id: Id): Boolean = values.contains(id) + def hasElement(id: Id): Boolean = this.synchronized { values.contains(id) } - def clear(): Unit = values = new HashMap[Id, T] + def clear(): Unit = this.synchronized { values.clear() } - def size: Int = values.size + def size: Int = this.synchronized { values.size } /** * Generates a new ID based on the existing entries. */ - private def generateId: Id = { + private def generateId(): Id = { var id = Id(0L) while (values.keySet.contains(id)) { id = Id(id.id + 1) } id 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 ec4c55a..ec514d5 100644 --- a/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala +++ b/src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala @@ -15,10 +15,10 @@ class StateImpl(sutState: SutState) extends State with Serializable { */ var transitions = new HashMap[Action, ActionTransitions] - override def getSutState: SutState = sutState - override def getTransitions: Map[Action, ActionTransitions] = transitions + override def getSutState: SutState = this.synchronized { sutState } + override def getTransitions: Map[Action, ActionTransitions] = this.synchronized { transitions } - private[api] override def addTransition(a: Action, to: State): Int = { + private[api] override def addTransition(a: Action, to: State): Int = this.synchronized { val old = transitions.get(a) old match { case Some(o) => 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 9815be7..aa1d3b6 100644 --- a/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/impl/GuiStateMachineImplSpec.scala @@ -91,7 +91,7 @@ class GuiStateMachineImplSpec extends AbstractApiSpec with BeforeAndAfterEach { "clear the state machine" in { sut.clear() sut.getAllExploredActions.isEmpty shouldEqual true - sut.actionExecutionTimes.isEmpty shouldEqual true + sut.getActionExecutionTimes.isEmpty shouldEqual true sut.getAllStates.isEmpty shouldEqual true } 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 474db9b..6891312 100644 --- a/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/api/impl/StateImplSpec.scala @@ -3,7 +3,6 @@ package de.retest.guistatemachine.api.impl import java.util.Arrays import de.retest.guistatemachine.api.AbstractApiSpec -import de.retest.surili.model.actions.NavigateToAction import de.retest.ui.descriptors.SutState class StateImplSpec extends AbstractApiSpec { @@ -11,8 +10,6 @@ class StateImplSpec extends AbstractApiSpec { private val rootElementB = getRootElement("b", 0) private val sutStateA = new SutState(Arrays.asList(rootElementA)) private val sutStateB = new SutState(Arrays.asList(rootElementB)) - private val action0 = new NavigateToAction("http://google.com") - private val action1 = new NavigateToAction("http://wikipedia.org") "StateImpl" should { "not equal" in {