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

Commit

Permalink
Support directories and add tests #19
Browse files Browse the repository at this point in the history
- Store Neo4J databases in a directory.
- Remove execution counter to simplify code.
- Reuse the sutState and action messages when creating identifiers.
- Simplify tests and reuse them for Neo4J.
  • Loading branch information
tdauth committed Apr 16, 2019
1 parent ac17cad commit 72bf923
Show file tree
Hide file tree
Showing 26 changed files with 386 additions and 381 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ Each state machine is represented by a separate graph database stored in a separ
The relationship types correspond to actions.
Each relation has the property "counter" which contains the execution counter of the action.

We cannot use an embdedded driver if we want to use the corresponding software such as Neo4J Desktop.
The embedded databases are stored into sub directories in the directory `$HOME/.retest/guistatemachines` by default.
Using an embedded database simplifies the usage of the API on a local machine and improves the performance.
However, we cannot use an embdedded driver if we want to use the corresponding software such as Neo4J Desktop.
apoc procedures have to be registered manually for an embedded database.

Useful software for Neo4J:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package de.retest.guistatemachine.api

import de.retest.surili.commons.actions.Action

class ActionIdentifier(hash: String) extends HashIdentifier(hash) {
var msg = s"ActionIdentifier[action=Unknown, hash=$hash]"
class ActionIdentifier(hash: String, val actionMsg: String = "Unknown") extends HashIdentifier(hash) {
val msg = s"ActionIdentifier[action=$actionMsg, hash=$hash]"

def this(action: Action) = {
this(HashIdentifier.sha256Hash(action))
msg = s"ActionIdentifier[action=${action.toString}, hash=$hash]"
this(HashIdentifier.sha256Hash(action), action.toString)
}
override def toString: String = msg
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ package de.retest.guistatemachine.api
* The corresponding symbol is not stored in this class but in the [[State]] from which the transitions are started or which the transitions lead to.
*
* @param states The states which the transitions lead to or start from. Since it is a NFA, there can be multiple states for the same symbol.
* @param executionCounter The number of times all transitions for the action have been executed from the corresponding state.
* It does not matter to which state. In the legacy code this was stored as `StateGraph.executionCounter`.
*/
@SerialVersionUID(1L)
case class ActionTransitions(states: Set[State], executionCounter: Int) extends Serializable
case class ActionTransitions(states: Set[State]) extends Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ trait GuiStateMachine {
* @param from The state the action is executed from
* @param a The action which is executed by the user.
* @param to The state which the execution leads to.
* @return The number of times the action has been executed.
*/
def executeAction(from: State, a: ActionIdentifier, to: State): Int = from.addTransition(a, to)
def executeAction(from: State, a: Action, to: State): Int = executeAction(from, new ActionIdentifier(a), to)
def executeAction(fromSutState: SutState, a: Action, toSutState: SutState): Int =
def executeAction(from: State, a: ActionIdentifier, to: State): Unit = from.addTransition(a, to)
def executeAction(from: State, a: Action, to: State): Unit = executeAction(from, new ActionIdentifier(a), to)
def executeAction(fromSutState: SutState, a: Action, toSutState: SutState): Unit =
executeAction(getState(fromSutState), a, getState(toSutState))

def getAllStates: Map[SutStateIdentifier, State]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package de.retest.guistatemachine.api
import java.nio.file.Paths

import de.retest.guistatemachine.api.impl.GuiStateMachineApiImpl
import de.retest.guistatemachine.api.neo4j.GuiStateMachineApiNeo4J

Expand Down Expand Up @@ -38,12 +40,18 @@ trait GuiStateMachineApi {
}

object GuiStateMachineApi {

/**
* The default directory where all state machines are stored.
*/
val StorageDirectory = Paths.get(System.getProperty("user.home"), ".retest", "guistatemachines").toAbsolutePath.toString

val default = new GuiStateMachineApiImpl

/**
* @return The standard implementaiton of the API.
*/
def apply(): GuiStateMachineApi = default

val neo4j = new GuiStateMachineApiNeo4J
val neo4j = new GuiStateMachineApiNeo4J(StorageDirectory)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package de.retest.guistatemachine.api

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

trait GuiStateMachineSerializer {
def save(filePath: String)
Expand Down
3 changes: 1 addition & 2 deletions src/main/scala/de/retest/guistatemachine/api/State.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ trait State {
*
* @param a The action which represents the transition's consumed symbol.
* @param to The state which the transition leads t o.
* @return The number of times the action has been executed from this state. The target state does not matter for this number.
*/
private[api] def addTransition(a: ActionIdentifier, to: State): Int
private[api] def addTransition(a: ActionIdentifier, to: State): Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package de.retest.guistatemachine.api

import de.retest.recheck.ui.descriptors.SutState

class SutStateIdentifier(hash: String) extends HashIdentifier(hash) {
var msg = s"SutStateIdentifier[sutState=Unknown, hash=$hash]"
class SutStateIdentifier(hash: String, val sutStateMsg: String = "Unknown") extends HashIdentifier(hash) {
val msg = s"SutStateIdentifier[sutState=$sutStateMsg, hash=$hash]"

def this(sutState: SutState) {
this(HashIdentifier.sha256Hash(sutState))
msg = s"SutStateIdentifier[sutState=${sutState.toString}, hash=$hash]"
this(HashIdentifier.sha256Hash(sutState), sutState.toString)
}
override def toString: String = msg
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ object Example extends App {
private val action0 = new NavigateToAction("http://google.com")
private val action1 = new NavigateToAction("http://wikipedia.org")

val stateMachine = GuiStateMachineApi.neo4j.createStateMachine("tmp")
val stateMachine = GuiStateMachineApi.neo4j.getStateMachine("tmp") match {
case Some(s) => s

case None => GuiStateMachineApi.neo4j.createStateMachine("tmp")
}

println(s"All states before clearing: ${stateMachine.getAllStates.size}")

stateMachine.clear()

println(s"All states after clearing: ${stateMachine.getAllStates.size}")
Expand Down
16 changes: 6 additions & 10 deletions src/main/scala/de/retest/guistatemachine/api/impl/StateImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,28 @@ case class StateImpl(sutState: SutStateIdentifier) extends State with Serializab
override def getOutgoingActionTransitions: Map[ActionIdentifier, ActionTransitions] = this.synchronized { outgoingActionTransitions }
override def getIncomingActionTransitions: Map[ActionIdentifier, ActionTransitions] = this.synchronized { incomingActionTransitions }

private[api] override def addTransition(a: ActionIdentifier, to: State): Int = {
val executionCounter = this.synchronized {
private[api] override def addTransition(a: ActionIdentifier, to: State): Unit = {
this.synchronized {
outgoingActionTransitions.get(a) match {
case Some(oldTransitions) =>
val updatedTransitions = ActionTransitions(oldTransitions.states + to, oldTransitions.executionCounter + 1)
val updatedTransitions = ActionTransitions(oldTransitions.states + to)
outgoingActionTransitions = outgoingActionTransitions + (a -> updatedTransitions)
updatedTransitions.executionCounter

case None =>
outgoingActionTransitions += (a -> ActionTransitions(Set(to), 1))
1
outgoingActionTransitions += (a -> ActionTransitions(Set(to)))
}
}

to.synchronized {
val other = to.asInstanceOf[StateImpl]
other.incomingActionTransitions.get(a) match {
case Some(oldTransitions) =>
val updatedTransitions = ActionTransitions(oldTransitions.states + this, oldTransitions.executionCounter + 1)
val updatedTransitions = ActionTransitions(oldTransitions.states + this)
other.incomingActionTransitions = other.incomingActionTransitions + (a -> updatedTransitions)

case None =>
other.incomingActionTransitions += (a -> ActionTransitions(Set(this), 1))
other.incomingActionTransitions += (a -> ActionTransitions(Set(this)))
}
}

executionCounter
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.neo4j.ogm.annotation._
@RelationshipEntity(`type` = "ACTIONS")
class ActionTransitionEntity(s: SutStateEntity, e: SutStateEntity, a: String, msg: String) {

def this(s: SutStateEntity, e: SutStateEntity, a: ActionIdentifier) = this(s, e, a.hash, a.msg)
def this(s: SutStateEntity, e: SutStateEntity, a: ActionIdentifier) = this(s, e, a.hash, a.actionMsg)
def this() = this(null, null, null, null)

@Id
Expand All @@ -26,16 +26,11 @@ class ActionTransitionEntity(s: SutStateEntity, e: SutStateEntity, a: String, ms

@Property(name = ActionTransitionEntity.PropertyMessage)
var message: String = msg

/// The number of times this action has been executed.
@Property(name = ActionTransitionEntity.PropertyNameCounter)
var counter: Int = 1
}

object ActionTransitionEntity {
final val PropertyNameStart = "start"
final val PropertyNameEnd = "end"
final val PropertyNameAction = "action"
final val PropertyMessage = "message"
final val PropertyNameCounter = "counter"
}
Original file line number Diff line number Diff line change
@@ -1,42 +1,51 @@
package de.retest.guistatemachine.api.neo4j

import java.io.File
import java.nio.file.Paths

import com.typesafe.scalalogging.Logger
import de.retest.guistatemachine.api.{GuiStateMachine, GuiStateMachineApi}
import org.apache.commons.io.FileUtils

import scala.collection.concurrent.TrieMap

class GuiStateMachineApiNeo4J extends GuiStateMachineApi {
class GuiStateMachineApiNeo4J(directory: String) extends GuiStateMachineApi {
private val logger = Logger[GuiStateMachineApiNeo4J]
private val stateMachines = TrieMap[String, GuiStateMachineNeo4J]()
// TODO #19 Load existing state machines based on Neo4J graph databases.

override def createStateMachine(name: String): GuiStateMachine = {
val uri = getUri(name)
Neo4jSessionFactory.getSessionFactoryEmbedded(uri)
logger.info("Created new graph DB in {}.", uri)
override def createStateMachine(name: String): GuiStateMachine =
if (isDirectory(name)) {
throw new RuntimeException(s"State machine $name does already exist.")
} else {
val uri = getUri(name)
Neo4jSessionFactory.getSessionFactoryEmbedded(uri)
logger.info("Created new graph DB in {}.", uri)

val guiStateMachine = new GuiStateMachineNeo4J(uri)
stateMachines += (name -> guiStateMachine)
guiStateMachine
}
new GuiStateMachineNeo4J(uri)
}

override def removeStateMachine(name: String): Boolean = stateMachines.remove(name) match {
case Some(stateMachine) =>
// TODO #19 Should we remove the state machine from the disk?
stateMachine.clear()
val uri = getUri(name)
Neo4jSessionFactory.getSessionFactoryEmbedded(uri).close()
override def removeStateMachine(name: String): Boolean =
if (isDirectory(name)) {
val file = getFile(name)
logger.info("Deleting state machine in {}.", file)
FileUtils.deleteDirectory(file)
true
case None => false
}

override def getStateMachine(name: String): Option[GuiStateMachine] = stateMachines.get(name)
} else {
false
}

override def clear(): Unit = stateMachines.keySet foreach { name => // TODO #19 keys can be modified concurrently. So we might not remove all state machines?
removeStateMachine(name)
override def getStateMachine(name: String): Option[GuiStateMachine] =
if (isDirectory(name)) {
val uri = getUri(name)
Some(new GuiStateMachineNeo4J(uri))
} else {
None
}

override def clear(): Unit = {
logger.info("Deleting all state machines in {}.", directory)
FileUtils.deleteDirectory(getStorageDirectory())
}

private def getUri(name: String): String = new File(name).toURI.toString
private def getStorageDirectory(): File = new File(directory)
private def isDirectory(name: String): Boolean = getFile(name).isDirectory
private def getFile(name: String): File = new File(Paths.get(directory, name).toAbsolutePath.toString)
private def getUri(name: String): String = getFile(name).toURI.toString
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class GuiStateMachineNeo4J(var uri: String) extends GuiStateMachine {

while (iterator.hasNext) {
val node = iterator.next()
val sutState = new SutStateIdentifier(node.hash)
val sutState = new SutStateIdentifier(node.hash, node.message)
result = result + (sutState -> StateNeo4J(sutState, this))
}
result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ object Neo4jSessionFactory {

def getSessionFactoryEmbedded(uri: String): SessionFactory = sessionFactories.get(uri) match {
case Some(sessionFactory) => sessionFactory
case None =>
case None =>
// TODO #19 This must not overwrite an existing database! Actually, one should use one shared session factory but we distinguish between directories.
val conf = new Configuration.Builder().uri(uri).build
val sessionFactory = new SessionFactory(conf, this.getClass.getPackage.getName)
sessionFactories += (uri -> sessionFactory)
Expand All @@ -33,7 +34,7 @@ object Neo4jSessionFactory {

def transaction[A](f: Session => A)(implicit uri: String): A = {
// We have to create a session for every transaction since sessions are not thread-safe.
val session = sessionFactories(uri).openSession()
val session = getSessionFactoryEmbedded(uri).openSession()
var txn: Option[Transaction] = None
try {
val transaction = session.beginTransaction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ case class StateNeo4J(sutStateIdentifier: SutStateIdentifier, guiStateMachine: G
val iterator = sutStateEntity.outgoingActionTransitions.iterator()
while (iterator.hasNext) {
val relationship = iterator.next()
val action = new ActionIdentifier(relationship.action)
val action = new ActionIdentifier(relationship.action, relationship.message)
val targetSutState = new SutStateIdentifier(relationship.end.hash)
val counter = relationship.counter
val actionTransitions = if (result.contains(action)) {
val existing = result(action)
ActionTransitions(existing.states ++ Set(StateNeo4J(targetSutState, guiStateMachine)), existing.executionCounter + counter)
ActionTransitions(existing.states ++ Set(StateNeo4J(targetSutState, guiStateMachine)))
} else {
ActionTransitions(Set(StateNeo4J(targetSutState, guiStateMachine)), counter)
ActionTransitions(Set(StateNeo4J(targetSutState, guiStateMachine)))
}
result = result + (action -> actionTransitions)
}
Expand All @@ -36,21 +35,20 @@ case class StateNeo4J(sutStateIdentifier: SutStateIdentifier, guiStateMachine: G
val iterator = sutStateEntity.incomingActionTransitions.iterator()
while (iterator.hasNext) {
val relationship = iterator.next()
val action = new ActionIdentifier(relationship.action)
val action = new ActionIdentifier(relationship.action, relationship.message)
val sourceSutState = new SutStateIdentifier(relationship.start.hash)
val counter = relationship.counter
val actionTransitions = if (result.contains(action)) {
val existing = result(action)
ActionTransitions(existing.states ++ Set(StateNeo4J(sourceSutState, guiStateMachine)), existing.executionCounter + counter)
ActionTransitions(existing.states ++ Set(StateNeo4J(sourceSutState, guiStateMachine)))
} else {
ActionTransitions(Set(StateNeo4J(sourceSutState, guiStateMachine)), counter)
ActionTransitions(Set(StateNeo4J(sourceSutState, guiStateMachine)))
}
result = result + (action -> actionTransitions)
}
result
}(guiStateMachine.uri)

private[api] override def addTransition(a: ActionIdentifier, to: State): Int =
private[api] override def addTransition(a: ActionIdentifier, to: State): Unit =
Neo4jSessionFactory.transaction { session =>
val sourceState = getSutStateEntity(session)
val targetSutStateIdentifier = to.asInstanceOf[StateNeo4J].sutStateIdentifier
Expand All @@ -60,13 +58,10 @@ case class StateNeo4J(sutStateIdentifier: SutStateIdentifier, guiStateMachine: G

if (matchingTransitions.nonEmpty) {
val first = matchingTransitions.head
first.counter = first.counter + 1
session.save(first)
first.counter
} else {
val transition = new ActionTransitionEntity(sourceState, targetState, a)
session.save(transition)
1
}
}(guiStateMachine.uri)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class SutStateEntity(@Property(name = SutStateEntity.PropertyNameHash)
var hash: java.lang.String,
msg: String) {

def this(sutStateIdentifier: SutStateIdentifier) = this(sutStateIdentifier.hash, sutStateIdentifier.msg)
def this(sutStateIdentifier: SutStateIdentifier) = this(sutStateIdentifier.hash, sutStateIdentifier.sutStateMsg)
def this() = this(null, null)

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

import de.retest.guistatemachine.api.{ActionIdentifier, SutStateIdentifier}

case class GraphActionEdge(from: SutStateIdentifier, to: SutStateIdentifier, action: ActionIdentifier) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine.api.impl.serialization
package de.retest.guistatemachine.api.serialization

import java.awt.Color

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

import java.io.{BufferedWriter, File, FileOutputStream, OutputStreamWriter}

import com.github.systemdir.gml.YedGmlWriter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine.api.impl.serialization
package de.retest.guistatemachine.api.serialization

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

Expand Down
Loading

0 comments on commit 72bf923

Please sign in to comment.