From a683ff0dc99bf32f05d0241debf0ad74e0d28b22 Mon Sep 17 00:00:00 2001 From: Tamino Dauth Date: Thu, 28 Mar 2019 15:25:27 +0100 Subject: [PATCH] Store actions as XML #19 --- build.sbt | 1 + .../api/neo4j/ActionConverter.scala | 112 +++++++++++++++++- .../api/neo4j/ActionTransitionEntity.scala | 11 +- .../api/neo4j/GuiStateMachineApiNeo4J.scala | 2 +- .../api/neo4j/GuiStateMachineNeo4J.scala | 2 +- .../api/neo4j/Neo4jSessionFactory.scala | 1 - .../api/neo4j/StateNeo4J.scala | 2 +- .../api/neo4j/SutStateEntity.scala | 2 +- .../api/neo4j/ActionConverterSpec.scala | 109 +++++++++++++++++ 9 files changed, 229 insertions(+), 13 deletions(-) create mode 100644 src/test/scala/de/retest/guistatemachine/api/neo4j/ActionConverterSpec.scala diff --git a/build.sbt b/build.sbt index ad03dbd..d7c138d 100644 --- a/build.sbt +++ b/build.sbt @@ -28,6 +28,7 @@ libraryDependencies += "javax.xml.bind" % "jaxb-api" % "2.3.0" libraryDependencies += "org.neo4j" % "neo4j" % "3.0.1" libraryDependencies += "org.neo4j" % "neo4j-ogm-core" % "3.1.7" libraryDependencies += "org.neo4j" % "neo4j-ogm-embedded-driver" % "3.1.7" +libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "1.1.1" // Dependencies to write GML files for yEd: libraryDependencies += "com.github.systemdir.gml" % "GMLWriterForYed" % "2.1.0" diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala index 703ecd2..e430873 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionConverter.scala @@ -1,9 +1,113 @@ package de.retest.guistatemachine.api.neo4j -import de.retest.surili.commons.actions.{Action, NavigateRefreshAction} +import de.retest.recheck.ui.descriptors.{Element, SutState} +import de.retest.surili.commons.actions._ import org.neo4j.ogm.typeconversion.AttributeConverter -class ActionConverter extends AttributeConverter[Action, String] { - def toGraphProperty(value: Action): String = value.toString // TODO #19 convert to XML with element - def toEntityAttribute(value: String): Action = new NavigateRefreshAction // TODO #19 convert from XML to an action +import scala.xml._ + +/** + * We do not want to store the whole target element as XML again. Hence, we store its retest ID which is unique and + * matches the element in the SUT state and only the additional attributes. + */ +class ActionConverter(val sutState: Option[SutState]) extends AttributeConverter[Action, String] { + + def this() = this(None) + + def toGraphProperty(value: Action): String = { + val nodeBuffer = new NodeBuffer + + nodeBuffer += {value.getClass.getSimpleName} + + if (value.getTargetElement.isPresent) { + val retestId = value.getTargetElement.get().getRetestId + nodeBuffer += {retestId} + } + + value match { + case a: ChangeValueOfAction => + val sequences = a.getKeysToSend map { sequence => + {sequence.toString} + } + nodeBuffer += {sequences} + case a: NavigateToAction => + nodeBuffer += {a.getUrl} + case a: SwitchToWindowAction => + nodeBuffer += {a.getWindowName} + case _ => + } + + val topLevelNode = {nodeBuffer} + val stringBuilder = new StringBuilder("\n") + val prettyPrinter = new PrettyPrinter(0, 2) + prettyPrinter.formatNodes(topLevelNode, TopScope, stringBuilder) + stringBuilder.toString() + } + def toEntityAttribute(value: String): Action = sutState match { + case Some(state) => + val node = scala.xml.XML.loadString(value) + val typeNode = getNodeByTag(node, "type") + typeNode.text match { + case "ChangeValueOfAction" => + val element = getElement(node, state) + val keys = getNodeByTag(node, "keys") + val sequences = keys.child map { c => + c.text + } + new ChangeValueOfAction(element, sequences.toArray) + case "ClickOnAction" => + val element = getElement(node, state) + new ClickOnAction(element) + case "NavigateToAction" => + val urlNode = getNodeByTag(node, "url") + new NavigateToAction(urlNode.text) + case "NavigateBackAction" => new NavigateBackAction + case "NavigateForwardAction" => new NavigateForwardAction + case "NavigateRefreshAction" => new NavigateRefreshAction + case "SwitchToWindowAction" => + val windowNode = getNodeByTag(node, "window") + new SwitchToWindowAction(windowNode.text) + case _ => throw new RuntimeException("Unknown type.") + } + + case None => throw new RuntimeException("We need the SutState to reconstruct the action") + } + + private def getNodeByTag(node: Node, tag: String): Node = { + val matchingNodes = node \\ tag + if (matchingNodes.isEmpty) { + throw new RuntimeException(s"Missing node with tag $tag.") + } else { + matchingNodes.head + } + } + + private def getElement(node: Node, sutState: SutState): Element = { + val retestId = getNodeByTag(node, "retestId").text + getElementByRetestId(retestId, sutState) match { + case Some(element) => element + case None => throw new RuntimeException(s"Missing element with retestId $retestId") + } + } + + private def getElementByRetestId(retestId: String, sutState: SutState): Option[Element] = { + val elements = scala.collection.mutable.Set[Element]() + val iterator = sutState.getRootElements.iterator() + var result: Option[Element] = None + + while (iterator.hasNext && result.isEmpty) { + val element = iterator.next() + result = if (element.getRetestId == retestId) { + Some(element) + } else { None } + + val nestedIterator = element.getContainedElements.iterator() + + while (nestedIterator.hasNext) { + elements += nestedIterator.next() + } + } + + result + } } diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala index 52a3763..2e16baa 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/ActionTransitionEntity.scala @@ -1,20 +1,23 @@ package de.retest.guistatemachine.api.neo4j import de.retest.recheck.ui.descriptors.SutState import de.retest.surili.commons.actions.Action -import org.neo4j.ogm.annotation.typeconversion.Convert -import org.neo4j.ogm.annotation.{EndNode, RelationshipEntity, StartNode} +import org.neo4j.ogm.annotation.{EndNode, Index, RelationshipEntity, StartNode} @RelationshipEntity(`type` = "ACTIONS") class ActionTransitionEntity(s: SutState, e: SutState, a: Action) extends Entity { def this() = this(null, null, null) + @Index @StartNode val start: SutState = s + @Index @EndNode val end: SutState = e - @Convert(classOf[ActionConverter]) - val action: Action = a + @Index + // TODO #19 We need the previous SutState for the conversion back to the action since we rely on the retest ID only to keep the action small. + //@Convert(classOf[ActionConverter]) + val actionXML: String = new ActionConverter().toGraphProperty(a) /// The number of times this action has been executed. var counter: Int = 1 diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala index f651dd1..4afc579 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineApiNeo4J.scala @@ -37,7 +37,7 @@ class GuiStateMachineApiNeo4J extends GuiStateMachineApi { override def getStateMachine(name: String): Option[GuiStateMachine] = stateMachines.get(name) - override def clear(): Unit = stateMachines.keySet foreach { name => // TODO #19 keys can be modified concurrently + 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) } // TODO #19 Removes from disk? diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala index 338daad..a0f225f 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/GuiStateMachineNeo4J.scala @@ -8,7 +8,7 @@ import org.neo4j.ogm.cypher.{ComparisonOperator, Filter} import scala.collection.immutable.HashMap class GuiStateMachineNeo4J(var uri: String) extends GuiStateMachine { - implicit val session = Neo4jSessionFactory.getSessionFactory(uri).openSession() // TODO #19 Save the session at some point and close it at some point + implicit val session = Neo4jSessionFactory.getSessionFactory(uri).openSession() // TODO #19 Close the session at some point? override def getState(sutState: SutState): State = { Neo4jSessionFactory.transaction { diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala index f74b28e..c5c4f5a 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/Neo4jSessionFactory.scala @@ -6,7 +6,6 @@ import org.neo4j.ogm.transaction.Transaction import scala.collection.concurrent.TrieMap -// TODO #19 Use sessions to modify the state graph. object Neo4jSessionFactory { private val sessionFactories = TrieMap[String, SessionFactory]() diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala index 339c93d..534fec3 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/StateNeo4J.scala @@ -18,7 +18,7 @@ case class StateNeo4J(sutState: SutState, guiStateMachine: GuiStateMachineNeo4J) val iterator = transitions.iterator() while (iterator.hasNext) { val relationship = iterator.next() - val action = relationship.action + val action = new ActionConverter(Some(relationship.start)).toEntityAttribute(relationship.actionXML) val targetSutState = relationship.end val counter = relationship.counter val actionTransitions = if (result.contains(action)) { diff --git a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala index 6000707..fbe6d6f 100644 --- a/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala +++ b/src/main/scala/de/retest/guistatemachine/api/neo4j/SutStateEntity.scala @@ -4,12 +4,12 @@ import de.retest.recheck.ui.descriptors.SutState import org.neo4j.ogm.annotation._ import org.neo4j.ogm.annotation.typeconversion.Convert -// TODO #19 Use this entity and sessions instead of manual transactions. @NodeEntity class SutStateEntity(state: SutState) extends Entity { def this() = this(null) + @Index(unique = true) @Convert(classOf[SutStateConverter]) val sutState: SutState = state } diff --git a/src/test/scala/de/retest/guistatemachine/api/neo4j/ActionConverterSpec.scala b/src/test/scala/de/retest/guistatemachine/api/neo4j/ActionConverterSpec.scala new file mode 100644 index 0000000..c0d8e0b --- /dev/null +++ b/src/test/scala/de/retest/guistatemachine/api/neo4j/ActionConverterSpec.scala @@ -0,0 +1,109 @@ +package de.retest.guistatemachine.api.neo4j + +import java.util + +import de.retest.guistatemachine.api.AbstractApiSpec +import de.retest.surili.commons.actions._ +import org.scalatest.BeforeAndAfterEach + +class ActionConverterSpec extends AbstractApiSpec with BeforeAndAfterEach { + private val rootElement = getRootElement("a", 0) + private val sutState = createSutState(rootElement) + private val cut = new ActionConverter(Some(sutState)) + + "ActionConverter" should { + + "convert ChangeValueOfAction" in { + val list = util.Arrays.asList("foo", "bar", "waa") + val cs = list.toArray(new Array[CharSequence](list.size)) + val action = new ChangeValueOfAction(rootElement, cs) + + val result = cut.toGraphProperty(action) + result shouldEqual + """ + |ChangeValueOfActionretestIdfoobarwaa + |""".stripMargin + + val loadedAction = cut.toEntityAttribute(result) + loadedAction shouldEqual action + } + + "convert ClickOnAction" in { + val action = new ClickOnAction(rootElement) + + val result = cut.toGraphProperty(action) + result shouldEqual + """ + |ClickOnActionretestId + |""".stripMargin + + val loadedAction = cut.toEntityAttribute(result) + loadedAction shouldEqual action + } + + "convert NavigateToAction" in { + val action = new NavigateToAction("http://google.com") + + val result = cut.toGraphProperty(action) + result shouldEqual + """ + |NavigateToActionhttp://google.com + |""".stripMargin + + val loadedAction = cut.toEntityAttribute(result) + loadedAction shouldEqual action + } + + "convert NavigateBackAction" in { + val action = new NavigateBackAction() + + val result = cut.toGraphProperty(action) + result shouldEqual + """ + |NavigateBackAction + |""".stripMargin + + val loadedAction = cut.toEntityAttribute(result) + loadedAction shouldEqual action + } + + "convert NavigateForwardAction" in { + val action = new NavigateForwardAction() + + val result = cut.toGraphProperty(action) + result shouldEqual + """ + |NavigateForwardAction + |""".stripMargin + + val loadedAction = cut.toEntityAttribute(result) + loadedAction shouldEqual action + } + + "convert NavigateRefreshAction" in { + val action = new NavigateRefreshAction() + + val result = cut.toGraphProperty(action) + result shouldEqual + """ + |NavigateRefreshAction + |""".stripMargin + + val loadedAction = cut.toEntityAttribute(result) + loadedAction shouldEqual action + } + + "convert SwitchToWindowAction" in { + val action = new SwitchToWindowAction("test") + + val result = cut.toGraphProperty(action) + result shouldEqual + """ + |SwitchToWindowActiontest + |""".stripMargin + + val loadedAction = cut.toEntityAttribute(result) + loadedAction shouldEqual action + } + } +}