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

Commit

Permalink
Fix #9 by adding persistence support
Browse files Browse the repository at this point in the history
- Do not use Selenium Action interface since we wait for a retest type like
AbstractAction. Instead use an ID to distinguish between actions.
- Remove unnecessary mock and Selenium dependencies.
- Add clear() methods to remove all state machines from the memory.
- Add save() and load() support for persistence.
  • Loading branch information
tdauth committed Nov 20, 2018
1 parent 63dc948 commit 8b34de6
Show file tree
Hide file tree
Showing 14 changed files with 192 additions and 65 deletions.
2 changes: 0 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ scalaVersion := "2.12.7"

// Dependencies to represent the input of states and actions:
libraryDependencies += "de.retest" % "retest-model" % "5.0.0" withSources () withJavadoc ()
libraryDependencies += "org.seleniumhq.selenium" % "selenium-java" % "2.35.0" withSources () withJavadoc ()

// Dependencies to provide a REST service:
libraryDependencies += "com.github.scopt" % "scopt_2.12" % "3.7.0"
Expand All @@ -26,7 +25,6 @@ libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "

// 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.rest.WebServer")
Expand Down
10 changes: 4 additions & 6 deletions src/main/scala/de/retest/guistatemachine/api/Action.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package de.retest.guistatemachine.api

/**
* Interaction from the user with the GUI.
* TODO #6 Use an abstract representation of actions from retest-model instead of Selenium. The legacy code used `ActionIdentifyingAttributes`.
* TODO #6 Use an abstract representation of actions from retest-model. The legacy code used `ActionIdentifyingAttributes`.
* Selenium action types like `org.openqa.selenium.interactions.Action` should not be used since we require an `equals`
* and `hashCode` method here to use the action as a key for transitions.
*/
case class Action(a: org.openqa.selenium.interactions.Action) {

// TODO #6 Convert abstract representation of actions into string.
override def toString: String = "Selenium Action"
}
case class Action(id: Id)
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ trait GuiStateMachine {
* In the legacy code this was only used to calculate [[getAllNeverExploredActions]].
* It could be used for the visualization of the NFA to see how often actions are executed.
*
* @return The number of times every explored action has been executed in the NFA.
* @return The number of times every explored action has been executed in the NFA. Never explored actions are not part of it.
*/
def getActionExecutionTimes: Map[Action, Int]

/**
* Clears all states, transitions and never explored actions etc.
*/
def clear(): Unit
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package de.retest.guistatemachine.api

/**
* 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 permantly, 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 All @@ -25,15 +32,24 @@ trait GuiStateMachineApi {
*/
def getStateMachine(id: Id): Option[GuiStateMachine]

/**
* 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 persist(): Unit
def save(filePath: String): Unit

/**
* Loads all state machines from the disk.
* Clears all current state machines and loads all state machines from the disk.
*
* @param filePath The file which the state machines are loaded from.
*/
def load(): Unit
def load(filePath: String): Unit
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
package de.retest.guistatemachine.api.impl

import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi, Id}
import java.io.{FileInputStream, FileOutputStream, ObjectInputStream, ObjectOutputStream}

import scala.collection.immutable.HashMap
import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi, Id}

object GuiStateMachineApiImpl extends GuiStateMachineApi {
val stateMachines = IdMap(new HashMap[Id, GuiStateMachine])
var stateMachines = IdMap[GuiStateMachine]

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

override def removeStateMachine(id: Id): Boolean = stateMachines.removeElement(id)

override def getStateMachine(id: Id): Option[GuiStateMachine] = stateMachines.getElement(id)

override def persist(): Unit = {
// TODO #9 store on the disk
override def clear(): Unit = stateMachines.clear()

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

override def load(): Unit = {
// TODO #9 Load from the disk
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,9 +1,13 @@
package de.retest.guistatemachine.api.impl

import de.retest.guistatemachine.api.{Action, Descriptors, GuiStateMachine, State}

import scala.collection.immutable.{HashMap, HashSet}
class GuiStateMachineImpl extends GuiStateMachine {
var states = new HashMap[Descriptors, State]

@SerialVersionUID(1L)
class GuiStateMachineImpl extends GuiStateMachine with Serializable {
// Make it accessable from the impl package for unit tests.
private[impl] var states = new HashMap[Descriptors, State]

/**
* In the legacy code we had `getAllNeverExploredActions` which had to collect them from all states and make sure they were never executed.
Expand Down Expand Up @@ -51,4 +55,11 @@ class GuiStateMachineImpl extends GuiStateMachine {
override def getAllExploredActions: Set[Action] = allExploredActions

override def getActionExecutionTimes: Map[Action, Int] = actionExecutionTimes

override def clear(): Unit = {
states = HashMap[Descriptors, State]()
allNeverExploredActions = HashSet[Action]()
allExploredActions = HashSet[Action]()
actionExecutionTimes = HashMap[Action, Int]()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import scala.collection.immutable.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.
*/
case class IdMap[T](var values: scala.collection.immutable.Map[Id, T] = new HashMap[Id, T]) {
@SerialVersionUID(1L)
case class IdMap[T]() extends Serializable {
var values = new HashMap[Id, T]

/**
* Generates a new ID based on the existing entries.
Expand All @@ -32,9 +34,13 @@ case class IdMap[T](var values: scala.collection.immutable.Map[Id, T] = new Hash
def getElement(id: Id): Option[T] = values.get(id)

def hasElement(id: Id): Boolean = values.contains(id)

def clear(): Unit = values = new HashMap[Id, T]
}

object IdMap {
def apply[T](): IdMap[T] = new IdMap[T]

def fromValues[T](v: T*): IdMap[T] = {
val r = IdMap[T]()
for (e <- v) r.addNewElement(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import de.retest.guistatemachine.api.{Action, ActionTransitions, Descriptors, St

import scala.collection.immutable.HashMap

class StateImpl(descriptors: Descriptors, var neverExploredActions: Set[Action]) extends State {
@SerialVersionUID(1L)
class StateImpl(descriptors: Descriptors, var neverExploredActions: Set[Action]) extends State with Serializable {

/**
* TODO #4 Currently, there is no MultiMap trait for immutable maps in the Scala standard library.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import java.util.{Arrays, Collections}

import de.retest.ui.descriptors._
import de.retest.ui.image.Screenshot
import org.scalamock.scalatest.MockFactory
import org.scalatest.{Matchers, WordSpec}

abstract trait AbstractApiSpec extends WordSpec with Matchers with MockFactory {
abstract trait AbstractApiSpec extends WordSpec with Matchers {

/**
* Creates a new identifying attributes collection which should only match other identifying attributes with the same ID.
Expand All @@ -34,6 +33,4 @@ abstract trait AbstractApiSpec extends WordSpec with Matchers with MockFactory {
0,
"My Window"
)

def getAction(): org.openqa.selenium.interactions.Action = mock[org.openqa.selenium.interactions.Action]
}
20 changes: 20 additions & 0 deletions src/test/scala/de/retest/guistatemachine/api/ActionSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package de.retest.guistatemachine.api

import org.scalatest.{Matchers, WordSpec}

class ActionSpec extends WordSpec with Matchers {

"Action" should {
"equal and not equal" in {
val action0 = Action(Id(0))
val action1 = Action(Id(1))
action0.equals(action0) shouldEqual true
action0.equals(action1) shouldEqual false
}

"toString" in {
val action0 = Action(Id(0))
action0.toString shouldEqual "Action(Id(0))"
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package de.retest.guistatemachine.api.impl

import de.retest.guistatemachine.api.{AbstractApiSpec, Id}
import java.io.File

import de.retest.guistatemachine.api.{AbstractApiSpec, Action, Descriptors, Id}

class GuiStateMachineApiImplSpec extends AbstractApiSpec {

Expand All @@ -24,5 +26,72 @@ class GuiStateMachineApiImplSpec extends AbstractApiSpec {
"remove a state machine" in {
GuiStateMachineApiImpl.removeStateMachine(stateMachineId) shouldBe true
}

"clear all state machines" in {
GuiStateMachineApiImpl.createStateMachine shouldEqual Id(0)
GuiStateMachineApiImpl.createStateMachine shouldEqual Id(1)
GuiStateMachineApiImpl.createStateMachine shouldEqual Id(2)
GuiStateMachineApiImpl.clear()
GuiStateMachineApiImpl.getStateMachine(Id(2)).isEmpty shouldEqual true
}

"save and load" in {
val filePath = "./test_state_machines"
val oldFile = new File(filePath)

if (oldFile.exists()) oldFile.delete() shouldEqual true

val rootElementA = getRootElement("a")
val rootElementB = getRootElement("b")
val rootElementC = getRootElement("c")
val action0 = Action(Id(0))
val action1 = Action(Id(1))

val initialDescriptors = Descriptors(Set(rootElementA, rootElementB, rootElementC))
val initialNeverExploredActions = Set(action0, action1)
val finalDescriptors = Descriptors(Set(rootElementC))
val finalNeverExploredActions = Set(action0, action1)

// Create the whole state machine:
GuiStateMachineApiImpl.clear
stateMachineId = GuiStateMachineApiImpl.createStateMachine
val stateMachine = GuiStateMachineApiImpl.getStateMachine(stateMachineId).get
val initialState = stateMachine.getState(initialDescriptors, initialNeverExploredActions)
val finalState = stateMachine.executeAction(initialState, action0, finalDescriptors, finalNeverExploredActions)

// Save all state machines:
GuiStateMachineApiImpl.save(filePath)
val f = new File(filePath)
f.exists() shouldEqual true
f.isDirectory shouldEqual false

// Load all state machines:
GuiStateMachineApiImpl.clear
GuiStateMachineApiImpl.load(filePath)

// Verify all loaded state machines:
val loadedStateMachineOp = GuiStateMachineApiImpl.getStateMachine(stateMachineId)
loadedStateMachineOp.isDefined shouldEqual true
val loadedStateMachine = loadedStateMachineOp.get.asInstanceOf[GuiStateMachineImpl]

loadedStateMachine.getAllExploredActions.size shouldEqual 1
loadedStateMachine.getAllNeverExploredActions.size shouldEqual 1
loadedStateMachine.getActionExecutionTimes(action0) shouldEqual 1
loadedStateMachine.getActionExecutionTimes.contains(action1) shouldEqual false
loadedStateMachine.states.size shouldEqual 2
val loadedInitialState = loadedStateMachine.states(initialDescriptors)
val loadedFinalState = loadedStateMachine.states(finalDescriptors)
loadedInitialState.getDescriptors shouldEqual initialDescriptors
loadedInitialState.getTransitions.size shouldEqual 1
loadedInitialState.getTransitions.contains(action0) shouldEqual true
loadedInitialState.getNeverExploredActions.size shouldEqual 1
val loadedTransition = loadedInitialState.getTransitions(action0)
loadedTransition.executionCounter shouldEqual 1
loadedTransition.to.size shouldEqual 1
loadedTransition.to.toList(0) shouldEqual loadedFinalState
loadedFinalState.getDescriptors shouldEqual finalDescriptors
loadedFinalState.getTransitions.isEmpty shouldEqual true
loadedFinalState.getNeverExploredActions shouldEqual finalNeverExploredActions
}
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package de.retest.guistatemachine.api.impl

import de.retest.guistatemachine.api.{AbstractApiSpec, Action, Descriptors}
import de.retest.guistatemachine.api.{AbstractApiSpec, Action, Descriptors, Id}

class GuiStateMachineImplSpec extends AbstractApiSpec {

val sut = new GuiStateMachineImpl
val rootElementA = getRootElement("a")
val rootElementB = getRootElement("b")
val rootElementC = getRootElement("c")
val action0Mock = getAction()
val action1Mock = getAction()
val action0 = Action(Id(0))
val action1 = Action(Id(1))

"GuiStateMachine" should {
"add two transitions to two new states for the same action and one transition to another state for another action" in {
Expand All @@ -21,45 +21,45 @@ class GuiStateMachineImplSpec extends AbstractApiSpec {

// execute action0Mock for the first time
val s0Descriptors = Descriptors(Set(rootElementA))
val s0 = sut.executeAction(initial, Action(action0Mock), s0Descriptors, getNeverExploredActions)
val s0 = sut.executeAction(initial, action0, s0Descriptors, getNeverExploredActions)
initial.getNeverExploredActions.size shouldEqual 1
initial.getTransitions.size shouldEqual 1
initial.getTransitions(Action(action0Mock)).to.size shouldEqual 1
initial.getTransitions(Action(action0Mock)).executionCounter shouldEqual 1
initial.getTransitions(action0).to.size shouldEqual 1
initial.getTransitions(action0).executionCounter shouldEqual 1
s0.getNeverExploredActions.size shouldEqual 2
s0.getTransitions.size shouldEqual 0
sut.getAllExploredActions.size shouldEqual 1
sut.getAllNeverExploredActions.size shouldEqual 1
sut.getActionExecutionTimes.get(Action(action0Mock)).isDefined shouldEqual true
sut.getActionExecutionTimes.get(Action(action0Mock)).get shouldEqual 1
sut.getActionExecutionTimes.get(action0).isDefined shouldEqual true
sut.getActionExecutionTimes.get(action0).get shouldEqual 1

// execute action0Mock for the second time
val s1Descriptors = Descriptors(Set(rootElementB))
val s1 = sut.executeAction(initial, Action(action0Mock), s1Descriptors, getNeverExploredActions)
val s1 = sut.executeAction(initial, action0, s1Descriptors, getNeverExploredActions)
initial.getNeverExploredActions.size shouldEqual 1
initial.getTransitions.size shouldEqual 1
initial.getTransitions(Action(action0Mock)).to.size shouldEqual 2
initial.getTransitions(Action(action0Mock)).executionCounter shouldEqual 2
initial.getTransitions(action0).to.size shouldEqual 2
initial.getTransitions(action0).executionCounter shouldEqual 2
s1.getNeverExploredActions.size shouldEqual 2
s1.getTransitions.size shouldEqual 0
sut.getAllExploredActions.size shouldEqual 1
sut.getAllNeverExploredActions.size shouldEqual 1
sut.getActionExecutionTimes.get(Action(action0Mock)).isDefined shouldEqual true
sut.getActionExecutionTimes.get(Action(action0Mock)).get shouldEqual 2
sut.getActionExecutionTimes.get(action0).isDefined shouldEqual true
sut.getActionExecutionTimes.get(action0).get shouldEqual 2

// execute action1Mock for the first time
val s2Descriptors = Descriptors(Set(rootElementC))
val s2 = sut.executeAction(initial, Action(action1Mock), s2Descriptors, getNeverExploredActions)
val s2 = sut.executeAction(initial, action1, s2Descriptors, getNeverExploredActions)
initial.getNeverExploredActions.size shouldEqual 0
initial.getTransitions.size shouldEqual 2
initial.getTransitions(Action(action1Mock)).to.size shouldEqual 1
initial.getTransitions(Action(action1Mock)).executionCounter shouldEqual 1
initial.getTransitions(action1).to.size shouldEqual 1
initial.getTransitions(action1).executionCounter shouldEqual 1
s2.getNeverExploredActions.size shouldEqual 2
s2.getTransitions.size shouldEqual 0
sut.getAllExploredActions.size shouldEqual 2
sut.getAllNeverExploredActions.size shouldEqual 0
sut.getActionExecutionTimes.get(Action(action1Mock)).isDefined shouldEqual true
sut.getActionExecutionTimes.get(Action(action1Mock)).get shouldEqual 1
sut.getActionExecutionTimes.get(action1).isDefined shouldEqual true
sut.getActionExecutionTimes.get(action1).get shouldEqual 1
}

"store a state for the second access" in {
Expand All @@ -71,5 +71,5 @@ class GuiStateMachineImplSpec extends AbstractApiSpec {
}

def getDescriptors: Descriptors = Descriptors(Set(rootElementA, rootElementB, rootElementC))
def getNeverExploredActions: Set[Action] = Set(Action(action0Mock), Action(action1Mock))
def getNeverExploredActions: Set[Action] = Set(action0, action1)
}
Loading

0 comments on commit 8b34de6

Please sign in to comment.