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

Commit

Permalink
Fix REST service #1
Browse files Browse the repository at this point in the history
- Fix marshalling and unmarshalling for ID based maps and add unit test.
- Allow deletion of applications and add unit test.
- Revert inheritance of Map which does not work with case classes.
- Add Bash scripts with REST calls.
  • Loading branch information
tdauth committed Oct 25, 2018
1 parent b400c42 commit 3a2e07c
Show file tree
Hide file tree
Showing 15 changed files with 117 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The directory [scripts](./scripts) contains a number of Bash scripts which use `
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").
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.**
Expand Down
2 changes: 2 additions & 0 deletions scripts/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 GET http://localhost:8888/application/0
2 changes: 2 additions & 0 deletions scripts/create-test-suite.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/application/0/create-test-suite
2 changes: 2 additions & 0 deletions scripts/delete-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 DELETE http://localhost:8888/application/0
2 changes: 2 additions & 0 deletions scripts/test-suite.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/application/0/test-suites/0
2 changes: 2 additions & 0 deletions scripts/test-suites.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/application/0/test-suites
22 changes: 7 additions & 15 deletions src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
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
import spray.json.JsValue
import spray.json.JsonFormat
import spray.json.RootJsonFormat

/**
* 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]] {
class JsonFormatForIdMap[T](implicit val jsonFormat0: JsonFormat[scala.collection.immutable.Map[String, T]], implicit val jsonFormat1: JsonFormat[T]) extends RootJsonFormat[Map[T]] {
override def write(obj: Map[T]): JsValue =
obj.values.map { field => (field._1.id.toString -> field._2) }.toJson
jsonFormat0.write(obj.values.map { field => (field._1.id.toString -> field._2) })

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) })
}
val map = jsonFormat0.read(json)
new Map[T](map.map { x => (Id(x._1.toLong) -> x._2) })
}
}
7 changes: 6 additions & 1 deletion src/main/scala/de/retest/guistatemachine/RestService.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ trait RestService {
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)
Expand Down Expand Up @@ -77,5 +76,11 @@ trait RestService {
complete(id)
}
}
} ~ delete {
pathPrefix("application" / LongNumber) { id =>
val r = persistence.deleteApplication(Id(id))
complete(StatusCodes.OK)
}
}

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

final case class GuiApplications(var values: scala.collection.immutable.Map[Id, GuiApplication]) extends Map[GuiApplication](values)
final case class GuiApplications(apps: Map[GuiApplication])
5 changes: 1 addition & 4 deletions src/main/scala/de/retest/guistatemachine/model/Map.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ package de.retest.guistatemachine.model
* [[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.
*/
class Map[T](var values: scala.collection.immutable.Map[Id, T]) {
case class Map[T](var values: scala.collection.immutable.Map[Id, T]) {
/**
* Generates a new ID based on the existing entries.
* TODO Generate IDs in a better way. Maybe random numbers until one unused element is found?
*/
def generateId: Id = this.synchronized { if (values.isEmpty) Id(0) else Id(values.keySet.max.id + 1) }

def setValues(v: scala.collection.immutable.Map[Id, T]) { values = v }
def getValues: scala.collection.immutable.Map[Id, T] = values
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package de.retest.guistatemachine.model

final case class TestSuites(var values: scala.collection.immutable.Map[Id, TestSuite]) extends Map[TestSuite](values)
final case class TestSuites(suites: Map[TestSuite])
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,34 @@ import de.retest.guistatemachine.model.Map
*/
class Persistence {
// database
private val guiApplications = GuiApplications(new HashMap[Id, GuiApplication])
private val guiApplications = GuiApplications(Map(new HashMap[Id, GuiApplication]))

def getApplications(): GuiApplications = guiApplications

def addApplication(): Id = {
val apps = guiApplications
apps.synchronized {
val id = apps.generateId
apps.setValues(apps.getValues + (id -> new GuiApplication(TestSuites(new HashMap[Id, TestSuite]))))
val id = apps.apps.generateId
apps.apps.values = apps.apps.values + (id -> new GuiApplication(TestSuites(Map(new HashMap[Id, TestSuite]))))
id
}
}
def getApplication(id: Id): Option[GuiApplication] = {
val apps = guiApplications
apps.synchronized {
if (apps.values.contains(id)) Some(apps.values(id)) else { None }
if (apps.apps.values.contains(id)) Some(apps.apps.values(id)) else { None }
}
}

def deleteApplication(id: Id): Boolean = {
val apps = guiApplications
apps.synchronized {
if (apps.apps.values.contains(id)) {
apps.apps.values = apps.apps.values - id
true
} else {
false
}
}
}

Expand All @@ -48,8 +60,8 @@ class Persistence {
case Some(x) => {
val testSuites = x.testSuites
testSuites.synchronized {
val id = testSuites.generateId
testSuites.setValues(testSuites.getValues + (id -> TestSuite()))
val id = testSuites.suites.generateId
testSuites.suites.values = testSuites.suites.values + (id -> TestSuite())
Some(id)
}
}
Expand All @@ -62,7 +74,7 @@ class Persistence {
case Some(x) => {
val testSuites = x.testSuites
testSuites.synchronized {
if (testSuites.values.contains(testSuiteId)) Some(testSuites.values(testSuiteId)) else None
if (testSuites.suites.values.contains(testSuiteId)) Some(testSuites.suites.values(testSuiteId)) else None
}
}
case None => None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package de.retest.guistatemachine

import org.scalatest.WordSpec
import org.scalatest.Matchers

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import spray.json.DefaultJsonProtocol._
import spray.json._
import de.retest.guistatemachine.model.TestSuites
import de.retest.guistatemachine.model.Id
import de.retest.guistatemachine.model.TestSuite
import scala.collection.immutable.HashMap
import de.retest.guistatemachine.model.Map

class JsonFormatForIdMapSpec extends WordSpec with Matchers {

implicit val idFormat = jsonFormat1(Id)
implicit val testSuiteFormat = jsonFormat0(TestSuite)
implicit val hashMapFormatTestSuites = new JsonFormatForIdMap[TestSuite]
implicit val testSuitesFormat = jsonFormat1(TestSuites)

"The JSON format" should {
"convert an empty test suite into JSON and back" in {
val testSuites = TestSuites(Map(new HashMap[Id, TestSuite]()))
val json = testSuites.toJson
json.toString shouldEqual "{\"suites\":{}}"
val transformedTestSuites = json.convertTo[TestSuites]
transformedTestSuites.suites.values.isEmpty shouldEqual true
}

"convert a test suite with elements into JSON and back" in {
val testSuites = TestSuites(Map(new HashMap[Id, TestSuite]()))
testSuites.suites.values = testSuites.suites.values + (Id(0) -> TestSuite())
val json = testSuites.toJson
json.toString shouldEqual "{\"suites\":{\"0\":{}}}"
val transformedTestSuites = json.convertTo[TestSuites]
transformedTestSuites.suites.values.isEmpty shouldEqual false
transformedTestSuites.suites.values.contains(Id(0)) shouldEqual true
}
}

}
32 changes: 25 additions & 7 deletions src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import de.retest.guistatemachine.model.TestSuites
import de.retest.guistatemachine.persistence.Persistence
import akka.http.scaladsl.model.MediaType
import akka.http.scaladsl.model.MediaTypes
import de.retest.guistatemachine.model.TestSuite
import akka.http.scaladsl.model.StatusCode

class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest with RestService {

Expand All @@ -34,7 +36,7 @@ class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest wit
handled shouldEqual true
mediaType shouldEqual MediaTypes.`application/json`
val r = responseAs[GuiApplications]
r.values.size shouldEqual 0
r.apps.values.size shouldEqual 0
}
}

Expand All @@ -45,18 +47,18 @@ class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest wit
}

"return an empty application for the GET request with the path /application/0" in {
Get("/applications/0") ~> sut ~> check {
// TODO Print response here
println("Response: " + responseAs[String])
Get("/application/0") ~> sut ~> check {
handled shouldEqual true
status shouldEqual StatusCodes.OK
val r = responseAs[GuiApplication]
r.testSuites.values.size shouldEqual 0
r.testSuites.suites.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 {
Get("/application/0/test-suites") ~> sut ~> check {
val r = responseAs[TestSuites]
r.values.size shouldEqual 0
r.suites.values.size shouldEqual 0
}
}

Expand All @@ -65,5 +67,21 @@ class RestServiceSpec extends WordSpec with Matchers with ScalatestRouteTest wit
responseAs[Id] shouldEqual Id(0)
}
}

"return an empty test suite for the GET request with the path /application/0/test-suite/0" in {
Get("/application/0/test-suite/0") ~> sut ~> check {
handled shouldEqual true
status shouldEqual StatusCodes.OK
val r = responseAs[TestSuite]
// TODO There is no content in a test suite at the moment
}
}

"return status OK for the DELETE request with the path /application/0" in {
Delete("/application/0") ~> sut ~> check {
status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual "OK"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ class PersistenceSpec extends WordSpec with Matchers {

"The pesistence" should {
"allow adding one test suite" in {
sut.getApplications().values.size shouldEqual 0
sut.getApplications().apps.values.size shouldEqual 0
sut.addApplication().id shouldEqual 0
sut.getApplications().values.size shouldEqual 1
sut.getTestSuites(Id(0)).get.values.size shouldEqual 0
sut.getApplications().apps.values.size shouldEqual 1
sut.getTestSuites(Id(0)).get.suites.values.size shouldEqual 0
sut.addTestSuite(Id(0)).get.id shouldEqual 0
sut.getTestSuites(Id(0)).get.values.size shouldEqual 1
sut.getTestSuites(Id(0)).get.suites.values.size shouldEqual 1
val s = sut.getTestSuite(Id(0), Id(0))
s.isEmpty shouldEqual false
}
Expand Down

0 comments on commit 3a2e07c

Please sign in to comment.