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

Commit

Permalink
REST API revision #1 #2
Browse files Browse the repository at this point in the history
- Add custom JSON formatter for our Map type
- Move Furrer model into seprate package since it is not valid
- Separate persistence layer into separate package
- Add Bash scripts for REST calls
- Simplify documentation
  • Loading branch information
tdauth committed Oct 25, 2018
1 parent a1b2892 commit 98d5d4a
Show file tree
Hide file tree
Showing 25 changed files with 268 additions and 201 deletions.
90 changes: 16 additions & 74 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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-<scalaversion>/gui-state-machine-api-assembly-<version>.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: <https://lampwww.epfl.ch/~michelou/scala/using-scala-from-java.html>
* 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 <https://www.seleniumhq.org/projects/ide/>, 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.
Expand All @@ -70,49 +57,4 @@ Some suggestions on how the REST API could look like:
* `/application/<long>` GET queries a registered GUI application
* `/application/<long>/test-suites` GET queries all test suites for an existing GUI application
* `/application/<long>/create-test-suite` POST registers a new test suite for an existing GUI application
* `/application/<long>/test-suite/<long>` 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: <https://github.com/ArchDev/akka-http-rest>
* <https://doc.akka.io/docs/akka-http/current/>
* <https://www.playframework.com/>
* <https://www.reddit.com/r/scala/comments/6izqac/akka_http_vs_play_ws_what_is_the_current_state/>
* <https://slides.com/leonardoregnier/deck#/>
* <https://nordicapis.com/8-frameworks-to-build-a-web-api-in-scala/>
* Akk HTTP test examples: <https://github.com/akka/akka-http/tree/master/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives>
* Replaced by Akka HTTP: <http://spray.io/>

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
* `/application/<long>/test-suite/<long>` GET queries a registered test suite for an existing GUI application
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions scripts/applications.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
curl -H "Content-Type: application/json" -X GET http://localhost:8888/applications
2 changes: 2 additions & 0 deletions scripts/create-application.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
curl -H "Content-Type: application/json" -X POST http://localhost:8888/create-application
30 changes: 30 additions & 0 deletions src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala
Original file line number Diff line number Diff line change
@@ -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) })
}
}
}
115 changes: 32 additions & 83 deletions src/main/scala/de/retest/guistatemachine/RestService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/main/scala/de/retest/guistatemachine/WebServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine.model
package de.retest.guistatemachine.furrermodel

/**
* A visible widget on a [[GuiWindow]].
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine.model
package de.retest.guistatemachine.furrermodel

/**
* A visible window which the user can interact with.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine.model
package de.retest.guistatemachine.furrermodel

class State {
private val windows = Set[GuiWindow]()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine.model
package de.retest.guistatemachine.furrermodel

class TestCase(initialState: State) {
private val actions = Seq[UIAction]()
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.retest.guistatemachine.furrermodel

trait UIAction {

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.retest.guistatemachine.model
package de.retest.guistatemachine.furrermodel

import scala.annotation.tailrec

Expand Down
Loading

0 comments on commit 98d5d4a

Please sign in to comment.