diff --git a/README.md b/README.md index 74ca525..a932eee 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # GUI State Machine API -Service for the creation and modification of state machines of GUI tests based on a genetic algorithm. -This [REST](https://en.wikipedia.org/wiki/Representational_state_transfer) service stores the NFA in a [graph database](https://en.wikipedia.org/wiki/Graph_database). +REST service for the creation and modification of nondeterministic finite automaton of GUI tests based on a genetic algorithm. The service hides the actual implementation and defines a fixed interface for calls. Therefore, calling systems do not depend on the concrete implementation and it can be mocked easily for tests. @@ -19,32 +18,20 @@ Use the command `sbt run` to start the REST service. Use the command `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`. -## Model Notation -An [NFA](https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton) represents the states of the GUI. -The transitions are possible GUI actions. -The model is based on the UI model from [Search-Based System Testing: High Coverage, No False Alarms][http://www.specmate.org/papers/2012-07-Search-basedSystemTesting-HighCoverageNoFalseAlarms.pdf] (section "4.5 UI Model"). -Whenever an unknown state is replaced by a newly discovered state, the model is updated. - -Open questions: - -* How is the state machine represented in the current version of retest? -* state machine != NFA != graph? It is definitely an NFA since multiple outoing transitions are allowed. -* REST -> HTTP what about the performance? Alternatively use a direct API in Scala? Should be possible from Java: -* What kind of analysises on the state machines are required except for the next possible actions? -* Whenever a new state is discovered, is the transition to the unknown state removed? Couldn't there be more states to be discovered? Doesn't matter? -* EFG model? -* How should the differences of the different versions of the state machines be stored? -* The demo of Retest allows recording and replaying tests. The same can be done with , so benefits are the automatic test generation/training and the difference testing? - -## Legacy code -The package `de.retest.graph` contains all legacy classes. -The package exists in version with GIT tag `retest-2.4.1`. - -## Definitions -These definitions have been made by Furrer in his master's thesis. -See section `2.3.1 Problemrepräsentation` for a detailed definition of the SUT model. -The primary source for the definitions is [Exploring Realistic Program Behavior](https://www.st.cs.uni- -saarland.de/publications/files/fgross-tr-2012.pdf). +## Eclipse Support +Use the command `sbt eclipse` to generate a project for Eclipse. + +## Bash Scripts for REST Calls +The directory [scripts](./scripts) contains a number of Bash scripts which use `curl` to send REST calls to a running server. + +## 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 the transitions. +If an action has not been executed, it leads to an unknown state. +The NFA is based on the UI model from [Search-Based System Testing: High Coverage, No False Alarms][http://www.specmate.org/papers/2012-07-Search-basedSystemTesting-HighCoverageNoFalseAlarms.pdf] (section "4.5 UI Model"). +Whenever an unknown state is replaced by a newly discovered state, the NFA has to be updated. + +**At the moment, the following definitions are incomplete and must be adapted to the actual implementation which calls this service.** ### Test Suite A test suite is a set of test cases. @@ -70,49 +57,4 @@ Some suggestions on how the REST API could look like: * `/application/` GET queries a registered GUI application * `/application//test-suites` GET queries all test suites for an existing GUI application * `/application//create-test-suite` POST registers a new test suite for an existing GUI application -* `/application//test-suite/` GET queries a registered test suite for an existing GUI application - -## APIs suggested in the project proposal of Surili -* [Selenium WebDriver](http://seleniumhq.org/docs/03_webdriver.jsp) -* [Gherkin](https://github.com/cucumber/cucumber/wiki/Gherkin) - -## REST Frameworks for Scala -* Good example of a REST service with Akka: -* -* -* -* -* -* Akk HTTP test examples: -* Replaced by Akka HTTP: - -If REST is too slow, Scala can still be called directly via Java. - -## NFA Frameworks -TODO Add some. - -## Graph Databases -[List of graph databases](https://en.wikipedia.org/wiki/Graph_database#List_of_graph_databases) - -### Neo4j -* https://neo4j.com/ ([Licensing](https://neo4j.com/licensing/)) -* https://github.com/neo4j/neo4j -* https://github.com/FaKod/neo4j-scala/ (Scala Wrapper) -* https://github.com/seancheatham/scala-graph -* https://neo4j.com/blog/neo4j-scala-introduction/ - -### TinkerPop3 -* http://tinkerpop.apache.org/ -* https://github.com/apache/tinkerpop -* https://users.scala-lang.org/t/scala-graph-combined-with-a-graph-db/2824 -* https://github.com/mpollmeier/gremlin-scala - -## Automatic Test Generation -* [EvoSuite](http://www.evosuite.org/) -* [Randoop](https://randoop.github.io/randoop/) -* [Guitar](https://sourceforge.net/projects/guitar/) - -## Literature -* [Search-Based System Testing: High Coverage, No False Alarms](https://dl.acm.org/citation.cfm?id=2336762) -* Machine Learning and Evolutionary Computing for GUI-based Regression Testing, Master's Thesis by Daniel Kraus -* Vergleichsstudie maschineller Testverfahren für GUI-basierte Systeme, Master's Thesis by Felix Furrer \ No newline at end of file +* `/application//test-suite/` GET queries a registered test suite for an existing GUI application \ No newline at end of file diff --git a/build.sbt b/build.sbt index aee92b6..1bb36f2 100644 --- a/build.sbt +++ b/build.sbt @@ -6,6 +6,7 @@ organization := "tdauth" scalaVersion := "2.12.7" +libraryDependencies += "io.spray" % "spray-json_2.12" % "1.3.4" libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.1.5" libraryDependencies += "com.typesafe.akka" %% "akka-http-core" % "10.1.5" libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.5.12" diff --git a/scripts/applications.sh b/scripts/applications.sh new file mode 100644 index 0000000..a61b8e8 --- /dev/null +++ b/scripts/applications.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -H "Content-Type: application/json" -X GET http://localhost:8888/applications \ No newline at end of file diff --git a/scripts/create-application.sh b/scripts/create-application.sh new file mode 100644 index 0000000..ad08926 --- /dev/null +++ b/scripts/create-application.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -H "Content-Type: application/json" -X POST http://localhost:8888/create-application \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala b/src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala new file mode 100644 index 0000000..ccfcef8 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala @@ -0,0 +1,30 @@ +package de.retest.guistatemachine + +import spray.json._ +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import spray.json.DefaultJsonProtocol._ + +import de.retest.guistatemachine.model.Id +import de.retest.guistatemachine.model.Map +import scala.collection.immutable.HashMap + +/** + * Transforms a [[Map]] into a `scala.collection.immutable.Map[String, T]`, so it can be converted into valid JSON. + * Besides, transforms a JSON object which is a `scala.collection.immutable.Map[String, T]` back into a [[Map]]. + * This transformer requires a JSON format for the type `K`. + */ +class JsonFormatForIdMap[T](implicit val jsonFormat: JsonFormat[T]) extends RootJsonFormat[Map[T]] { + override def write(obj: Map[T]): JsValue = + obj.values.map { field => (field._1.id.toString -> field._2) }.toJson + + override def read(json: JsValue): Map[T] = { + val obj = json.asJsObject + if (obj.fields.isEmpty) { + new Map[T](new HashMap[Id, T]()) + // TODO Fix conversation back into the Map type. + } else { + val map = json.asInstanceOf[scala.collection.immutable.Map[String, T]] + new Map[T](map.map { x => (Id(x._1.toLong) -> x._2) }) + } + } +} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/RestService.scala b/src/main/scala/de/retest/guistatemachine/RestService.scala index ddbe996..f79fc8f 100644 --- a/src/main/scala/de/retest/guistatemachine/RestService.scala +++ b/src/main/scala/de/retest/guistatemachine/RestService.scala @@ -9,108 +9,57 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import spray.json.DefaultJsonProtocol._ +import spray.json._ -import java.util.LinkedList - -// domain model -// TODO #1 Id should use Long and the REST paths as well. Use concurrent hash maps with the IDs and generate new IDs for new items. -final case class Id(id: Int) -final case class GuiApplication() { - val testSuites = TestSuites() -} -final case class GuiApplications() { - val applications = new LinkedList[GuiApplication] -} -final case class TestSuite() -final case class TestSuites() { - val testSuites = new LinkedList[TestSuite] -} +import de.retest.guistatemachine.persistence.Persistence +import de.retest.guistatemachine.model.GuiApplications +import de.retest.guistatemachine.model.GuiApplication +import de.retest.guistatemachine.model.TestSuite +import de.retest.guistatemachine.model.TestSuites +import de.retest.guistatemachine.model.Id +import de.retest.guistatemachine.persistence.Persistence trait RestService { implicit val system: ActorSystem implicit val materializer: ActorMaterializer - // database - val guiApplications = GuiApplications() - - def addApplication(): Id = { - val apps = guiApplications.applications - apps.synchronized { - apps.add(new GuiApplication) - Id(apps.size() - 1) - } - } - def getApplication(id: Id): Option[GuiApplication] = { - val apps = guiApplications.applications - apps.synchronized { - val index = id.id - if (index >= 0 && index < apps.size()) Some(apps.get(index)) else None - } - } - - def getTestSuites(applicationId: Id): Option[TestSuites] = { - val app = getApplication(applicationId) - app match { - case Some(x) => Some(x.testSuites) - case None => None - } - } - - def addTestSuite(applicationId: Id): Option[Id] = { - val app = getApplication(applicationId) - app match { - case Some(x) => { - val testSuites = x.testSuites.testSuites - testSuites.synchronized { - testSuites.add(new TestSuite) - Some(Id(testSuites.size() - 1)) - } - } - case None => None - } - } - def getTestSuite(applicationId: Id, testSuiteId: Id): Option[TestSuite] = { - val app = getApplication(applicationId) - app match { - case Some(x) => { - val testSuites = x.testSuites.testSuites - testSuites.synchronized { - val index = testSuiteId.id - if (index >= 0 && index < testSuites.size()) Some(testSuites.get(index)) else None - } - } - case None => None - } - } - // formats for unmarshalling and marshalling implicit val idFormat = jsonFormat1(Id) - implicit val applicationFormat = jsonFormat0(GuiApplication) - implicit val applicationsFormat = jsonFormat0(GuiApplications) implicit val testSuiteFormat = jsonFormat0(TestSuite) - implicit val testSuitesFormat = jsonFormat0(TestSuites) + implicit val hashMapFormatTestSuites = new JsonFormatForIdMap[TestSuite] + implicit val testSuitesFormat = jsonFormat1(TestSuites) + implicit val applicationFormat = jsonFormat1(GuiApplication) + implicit val hashMapFormatApplications = new JsonFormatForIdMap[GuiApplication] + implicit val applicationsFormat = jsonFormat1(GuiApplications) - val route: Route = + /** + * Creates the complete route for the REST service with all possible paths. + */ + def getRoute(persistence: Persistence): Route = get { - path("applications") { - complete(guiApplications) + pathSingleSlash { + complete("GUI State Machine API") } ~ - pathPrefix("application" / IntNumber) { id => - val app = getApplication(Id(id)) + path("applications") { + complete(persistence.getApplications()) + } ~ + pathPrefix("application" / LongNumber) { id => + println("Getting application with ID " + id) + val app = persistence.getApplication(Id(id)) app match { case Some(x) => complete(x) case None => complete(StatusCodes.NotFound) } } ~ - pathPrefix("application" / IntNumber / "test-suites") { id => - val testSuites = getTestSuites(Id(id)) + pathPrefix("application" / LongNumber / "test-suites") { id => + val testSuites = persistence.getTestSuites(Id(id)) testSuites match { case Some(x) => complete(x) case None => complete(StatusCodes.NotFound) } } ~ - pathPrefix("application" / IntNumber / "test-suite" / IntNumber) { (appId, suiteId) => - val suite = getTestSuite(Id(appId), Id(suiteId)) + pathPrefix("application" / LongNumber / "test-suite" / LongNumber) { (appId, suiteId) => + val suite = persistence.getTestSuite(Id(appId), Id(suiteId)) suite match { case Some(x) => complete(x) case None => complete(StatusCodes.NotFound) @@ -119,12 +68,12 @@ trait RestService { } ~ post { path("create-application") { - val id = addApplication() + val id = persistence.addApplication() complete(id) } ~ - pathPrefix("application" / IntNumber / "create-test-suite") { appId => + pathPrefix("application" / LongNumber / "create-test-suite") { appId => { - val id = addTestSuite(Id(appId)) + val id = persistence.addTestSuite(Id(appId)) complete(id) } } diff --git a/src/main/scala/de/retest/guistatemachine/WebServer.scala b/src/main/scala/de/retest/guistatemachine/WebServer.scala index cc1ec57..fe07191 100644 --- a/src/main/scala/de/retest/guistatemachine/WebServer.scala +++ b/src/main/scala/de/retest/guistatemachine/WebServer.scala @@ -5,16 +5,22 @@ import scala.io.StdIn import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.stream.ActorMaterializer +import de.retest.guistatemachine.persistence.Persistence object WebServer extends App with RestService { + final val HOST = "localhost" + final val PORT = 8888 + implicit val system = ActorSystem("gui-state-machine-api-system") implicit val materializer = ActorMaterializer() // needed for the future flatMap/onComplete in the end implicit val executionContext = system.dispatcher - val bindingFuture = Http().bindAndHandle(route, "localhost", 8080) + val persistence = new Persistence + + val bindingFuture = Http().bindAndHandle(getRoute(persistence), HOST, PORT) - println(s"Server online at http://localhost:8080/\nPress RETURN to stop...") + println(s"Server online at http://${HOST}:${PORT}/\nPress RETURN to stop...") StdIn.readLine() // let it run until user presses return bindingFuture .flatMap(_.unbind()) // trigger unbinding from the port diff --git a/src/main/scala/de/retest/guistatemachine/furrermodel/GuiApplication.scala b/src/main/scala/de/retest/guistatemachine/furrermodel/GuiApplication.scala new file mode 100644 index 0000000..39797e6 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/furrermodel/GuiApplication.scala @@ -0,0 +1,13 @@ +package de.retest.guistatemachine.furrermodel + +/** + * The tested GUI application with an initial state and a number of test suites. + * + * @param id This ID is for the REST API only. + */ +class GuiApplication(val id : Long, initialState: State, testSuites: Seq[TestSuite]) { + + def getTestSuites: Seq[TestSuite] = testSuites + + def getInitialState: State = initialState +} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/model/GuiWidget.scala b/src/main/scala/de/retest/guistatemachine/furrermodel/GuiWidget.scala similarity index 75% rename from src/main/scala/de/retest/guistatemachine/model/GuiWidget.scala rename to src/main/scala/de/retest/guistatemachine/furrermodel/GuiWidget.scala index 116d90e..23fb920 100644 --- a/src/main/scala/de/retest/guistatemachine/model/GuiWidget.scala +++ b/src/main/scala/de/retest/guistatemachine/furrermodel/GuiWidget.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine.model +package de.retest.guistatemachine.furrermodel /** * A visible widget on a [[GuiWindow]]. diff --git a/src/main/scala/de/retest/guistatemachine/model/GuiWindow.scala b/src/main/scala/de/retest/guistatemachine/furrermodel/GuiWindow.scala similarity index 79% rename from src/main/scala/de/retest/guistatemachine/model/GuiWindow.scala rename to src/main/scala/de/retest/guistatemachine/furrermodel/GuiWindow.scala index c9dc92f..1ae5c8c 100644 --- a/src/main/scala/de/retest/guistatemachine/model/GuiWindow.scala +++ b/src/main/scala/de/retest/guistatemachine/furrermodel/GuiWindow.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine.model +package de.retest.guistatemachine.furrermodel /** * A visible window which the user can interact with. diff --git a/src/main/scala/de/retest/guistatemachine/model/State.scala b/src/main/scala/de/retest/guistatemachine/furrermodel/State.scala similarity index 79% rename from src/main/scala/de/retest/guistatemachine/model/State.scala rename to src/main/scala/de/retest/guistatemachine/furrermodel/State.scala index b294cd7..409da29 100644 --- a/src/main/scala/de/retest/guistatemachine/model/State.scala +++ b/src/main/scala/de/retest/guistatemachine/furrermodel/State.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine.model +package de.retest.guistatemachine.furrermodel class State { private val windows = Set[GuiWindow]() diff --git a/src/main/scala/de/retest/guistatemachine/model/TestCase.scala b/src/main/scala/de/retest/guistatemachine/furrermodel/TestCase.scala similarity index 87% rename from src/main/scala/de/retest/guistatemachine/model/TestCase.scala rename to src/main/scala/de/retest/guistatemachine/furrermodel/TestCase.scala index b1bdd64..5792913 100644 --- a/src/main/scala/de/retest/guistatemachine/model/TestCase.scala +++ b/src/main/scala/de/retest/guistatemachine/furrermodel/TestCase.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine.model +package de.retest.guistatemachine.furrermodel class TestCase(initialState: State) { private val actions = Seq[UIAction]() diff --git a/src/main/scala/de/retest/guistatemachine/furrermodel/TestSuite.scala b/src/main/scala/de/retest/guistatemachine/furrermodel/TestSuite.scala new file mode 100644 index 0000000..2ea30c7 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/furrermodel/TestSuite.scala @@ -0,0 +1,9 @@ +package de.retest.guistatemachine.furrermodel + +class TestSuite { + private val cases = Set[TestCase]() + + def size = cases.size + + def length = cases.foldLeft(0)((size, c) => size + c.length) +} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/furrermodel/UIAction.scala b/src/main/scala/de/retest/guistatemachine/furrermodel/UIAction.scala new file mode 100644 index 0000000..87510cc --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/furrermodel/UIAction.scala @@ -0,0 +1,5 @@ +package de.retest.guistatemachine.furrermodel + +trait UIAction { + +} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/model/UIPath.scala b/src/main/scala/de/retest/guistatemachine/furrermodel/UIPath.scala similarity index 93% rename from src/main/scala/de/retest/guistatemachine/model/UIPath.scala rename to src/main/scala/de/retest/guistatemachine/furrermodel/UIPath.scala index d09b847..8477557 100644 --- a/src/main/scala/de/retest/guistatemachine/model/UIPath.scala +++ b/src/main/scala/de/retest/guistatemachine/furrermodel/UIPath.scala @@ -1,4 +1,4 @@ -package de.retest.guistatemachine.model +package de.retest.guistatemachine.furrermodel import scala.annotation.tailrec diff --git a/src/main/scala/de/retest/guistatemachine/model/GuiApplication.scala b/src/main/scala/de/retest/guistatemachine/model/GuiApplication.scala index 9961e6e..f7a5189 100644 --- a/src/main/scala/de/retest/guistatemachine/model/GuiApplication.scala +++ b/src/main/scala/de/retest/guistatemachine/model/GuiApplication.scala @@ -1,13 +1,3 @@ package de.retest.guistatemachine.model -/** - * The tested GUI application with an initial state and a number of test suites. - * - * @param id This ID is for the REST API only. - */ -class GuiApplication(val id : Long, initialState: State, testSuites: Seq[TestSuite]) { - - def getTestSuites: Seq[TestSuite] = testSuites - - def getInitialState: State = initialState -} \ No newline at end of file +final case class GuiApplication(testSuites: TestSuites) \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/model/GuiApplications.scala b/src/main/scala/de/retest/guistatemachine/model/GuiApplications.scala new file mode 100644 index 0000000..9da2b30 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/model/GuiApplications.scala @@ -0,0 +1,6 @@ +package de.retest.guistatemachine.model + +final case class GuiApplications(applications: Map[GuiApplication]) { + // TODO Generate IDs in a better way. Maybe random numbers until one unused element is found? + def generateId: Id = this.synchronized { if (applications.values.isEmpty) Id(0) else Id(applications.values.keySet.max.id + 1) } +} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/model/Id.scala b/src/main/scala/de/retest/guistatemachine/model/Id.scala new file mode 100644 index 0000000..a02f591 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/model/Id.scala @@ -0,0 +1,7 @@ +package de.retest.guistatemachine.model + +final case class Id(val id: Long) extends Ordered[Id] { + import scala.math.Ordered.orderingToOrdered + + override def compare(that: Id): Int = this.id compare that.id +} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/model/Map.scala b/src/main/scala/de/retest/guistatemachine/model/Map.scala new file mode 100644 index 0000000..b67cf77 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/model/Map.scala @@ -0,0 +1,10 @@ +package de.retest.guistatemachine.model + +/** + * This custom type allows storing values using [[Id]] as key. + * [[JsonFormatForIdMap]] implements marshalling and unmarshalling for JSON for this type. + * We cannot extend immutable maps in Scala, so we have to keep it as field. + */ +case class Map[T](var values: scala.collection.immutable.Map[Id, T]) { + +} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/model/TestSuite.scala b/src/main/scala/de/retest/guistatemachine/model/TestSuite.scala index 435f304..27b104f 100644 --- a/src/main/scala/de/retest/guistatemachine/model/TestSuite.scala +++ b/src/main/scala/de/retest/guistatemachine/model/TestSuite.scala @@ -1,9 +1,3 @@ package de.retest.guistatemachine.model -class TestSuite { - private val cases = Set[TestCase]() - - def size = cases.size - - def length = cases.foldLeft(0)((size, c) => size + c.length) -} \ No newline at end of file +final case class TestSuite() \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/model/TestSuites.scala b/src/main/scala/de/retest/guistatemachine/model/TestSuites.scala new file mode 100644 index 0000000..52c855c --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/model/TestSuites.scala @@ -0,0 +1,6 @@ +package de.retest.guistatemachine.model + +final case class TestSuites(testSuites: Map[TestSuite]) { + // TODO Generate IDs in a better way. Maybe random numbers until one unused element is found? + def generateId: Id = this.synchronized { if (testSuites.values.isEmpty) Id(0) else Id(testSuites.values.keySet.max.id + 1) } +} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/model/UIAction.scala b/src/main/scala/de/retest/guistatemachine/model/UIAction.scala deleted file mode 100644 index ea12ee5..0000000 --- a/src/main/scala/de/retest/guistatemachine/model/UIAction.scala +++ /dev/null @@ -1,5 +0,0 @@ -package de.retest.guistatemachine.model - -trait UIAction { - -} \ No newline at end of file diff --git a/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala b/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala new file mode 100644 index 0000000..73c7d51 --- /dev/null +++ b/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala @@ -0,0 +1,71 @@ +package de.retest.guistatemachine.persistence + +import scala.collection.immutable.HashMap + +import de.retest.guistatemachine.model.GuiApplication +import de.retest.guistatemachine.model.GuiApplications +import de.retest.guistatemachine.model.Id +import de.retest.guistatemachine.model.TestSuite +import de.retest.guistatemachine.model.TestSuites +import de.retest.guistatemachine.model.Map + +/** + * Allows concurrent access to the persistence of the resources. + * The actual persistence layer is hidden by this class. + */ +class Persistence { + // database + private val guiApplications = GuiApplications(Map[GuiApplication](new HashMap[Id, GuiApplication])) + + def getApplications(): GuiApplications = guiApplications + + def addApplication(): Id = { + val apps = guiApplications + apps.synchronized { + val id = apps.generateId + apps.applications.values = apps.applications.values + (id -> new GuiApplication(TestSuites(Map[TestSuite](new HashMap[Id, TestSuite])))) + id + } + } + def getApplication(id: Id): Option[GuiApplication] = { + val apps = guiApplications + apps.synchronized { + if (apps.applications.values.contains(id)) Some(apps.applications.values(id)) else { None } + } + } + + def getTestSuites(applicationId: Id): Option[TestSuites] = { + val app = getApplication(applicationId) + app match { + case Some(x) => Some(x.testSuites) + case None => None + } + } + + def addTestSuite(applicationId: Id): Option[Id] = { + val app = getApplication(applicationId) + app match { + case Some(x) => { + val testSuites = x.testSuites + testSuites.synchronized { + val id = testSuites.generateId + testSuites.testSuites.values = testSuites.testSuites.values + (id -> TestSuite()) + Some(id) + } + } + case None => None + } + } + def getTestSuite(applicationId: Id, testSuiteId: Id): Option[TestSuite] = { + val app = getApplication(applicationId) + app match { + case Some(x) => { + val testSuites = x.testSuites + testSuites.synchronized { + if (testSuites.testSuites.values.contains(testSuiteId)) Some(testSuites.testSuites.values(testSuiteId)) else None + } + } + case None => None + } + } +} \ No newline at end of file diff --git a/src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala b/src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala index d60a892..861b055 100644 --- a/src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala @@ -9,16 +9,32 @@ import akka.http.scaladsl.model.HttpEntity import akka.http.scaladsl.model.ContentTypes import akka.http.scaladsl.model.HttpCharset import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ +import de.retest.guistatemachine.model.GuiApplications +import de.retest.guistatemachine.model.Id +import de.retest.guistatemachine.model.GuiApplication +import de.retest.guistatemachine.model.TestSuites +import de.retest.guistatemachine.persistence.Persistence +import akka.http.scaladsl.model.MediaType +import akka.http.scaladsl.model.MediaTypes class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest with RestService { - lazy val sut = route + val sut = getRoute(new Persistence) "The service" should { + "show the default text for the GET request with the path /" in { + Get("/") ~> sut ~> check { + val r = responseAs[String] + r shouldEqual "GUI State Machine API" + } + } + "return an empty list for the GET request with the path /applications" in { Get("/applications") ~> sut ~> check { + handled shouldEqual true + mediaType shouldEqual MediaTypes.`application/json` val r = responseAs[GuiApplications] - r.applications.size shouldEqual 0 + r.applications.values.size shouldEqual 0 } } @@ -28,34 +44,25 @@ class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest wit } } - /* - TODO #1 Somehow the current state of the REST service is not stored after creating one application. "return an empty application for the GET request with the path /application/0" in { Get("/applications/0") ~> sut ~> check { + // TODO Print response here val r = responseAs[GuiApplication] - r.testSuites.testSuites.size shouldEqual 0 + r.testSuites.testSuites.values.size shouldEqual 0 } } "return an empty list for the GET request with the path /application/0/test-suites" in { Get("/applications/0/test-suites") ~> sut ~> check { val r = responseAs[TestSuites] - r.testSuites.size shouldEqual 0 + r.testSuites.values.size shouldEqual 0 } } - */ "allow POST for path /application/0/create-test-suite" in { Post("/application/0/create-test-suite") ~> sut ~> check { responseAs[Id] shouldEqual Id(0) } } - - "not find any root path" in { - Get() ~> Route.seal(sut) ~> check { - status shouldEqual StatusCodes.NotFound - responseAs[String] shouldEqual "The requested resource could not be found." - } - } } } diff --git a/src/test/scala/de/retest/guistatemachine/persistence/PersistenceSpec.scala b/src/test/scala/de/retest/guistatemachine/persistence/PersistenceSpec.scala new file mode 100644 index 0000000..3efaf75 --- /dev/null +++ b/src/test/scala/de/retest/guistatemachine/persistence/PersistenceSpec.scala @@ -0,0 +1,22 @@ +package de.retest.guistatemachine.persistence + +import org.scalatest.Matchers +import org.scalatest.WordSpec +import de.retest.guistatemachine.model.Id + +class PersistenceSpec extends WordSpec with Matchers { + val sut = new Persistence + + "The pesistence" should { + "allow adding one test suite" in { + sut.getApplications().applications.values.size shouldEqual 0 + sut.addApplication().id shouldEqual 0 + sut.getApplications().applications.values.size shouldEqual 1 + sut.getTestSuites(Id(0)).get.testSuites.values.size shouldEqual 0 + sut.addTestSuite(Id(0)).get.id shouldEqual 0 + sut.getTestSuites(Id(0)).get.testSuites.values.size shouldEqual 1 + val s = sut.getTestSuite(Id(0), Id(0)) + s.isEmpty shouldEqual false + } + } +} \ No newline at end of file