From 3a2e07cb963b4a7b8a6a4c9ff61f5fca52967281 Mon Sep 17 00:00:00 2001 From: Tamino Dauth Date: Thu, 25 Oct 2018 18:18:28 +0200 Subject: [PATCH] Fix REST service #1 - 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. --- README.md | 2 +- scripts/application.sh | 2 + scripts/create-test-suite.sh | 2 + scripts/delete-application.sh | 2 + scripts/test-suite.sh | 2 + scripts/test-suites.sh | 2 + .../guistatemachine/JsonFormatForIdMap.scala | 22 ++++------ .../retest/guistatemachine/RestService.scala | 7 +++- .../model/GuiApplications.scala | 2 +- .../de/retest/guistatemachine/model/Map.scala | 5 +-- .../guistatemachine/model/TestSuites.scala | 2 +- .../persistence/Persistence.scala | 26 ++++++++---- .../JsonFormatForIdMapSpec.scala | 42 +++++++++++++++++++ .../guistatemachine/RestServiceSpec.scala | 32 ++++++++++---- .../persistence/PersistenceSpec.scala | 8 ++-- 15 files changed, 117 insertions(+), 41 deletions(-) create mode 100644 scripts/application.sh create mode 100644 scripts/create-test-suite.sh create mode 100644 scripts/delete-application.sh create mode 100644 scripts/test-suite.sh create mode 100644 scripts/test-suites.sh create mode 100644 src/test/scala/de/retest/guistatemachine/JsonFormatForIdMapSpec.scala diff --git a/README.md b/README.md index a932eee..45d18ec 100644 --- a/README.md +++ b/README.md @@ -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.** diff --git a/scripts/application.sh b/scripts/application.sh new file mode 100644 index 0000000..21d8fa2 --- /dev/null +++ b/scripts/application.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -H "Content-Type: application/json" -X GET http://localhost:8888/application/0 \ No newline at end of file diff --git a/scripts/create-test-suite.sh b/scripts/create-test-suite.sh new file mode 100644 index 0000000..cb967d2 --- /dev/null +++ b/scripts/create-test-suite.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -H "Content-Type: application/json" -X POST http://localhost:8888/application/0/create-test-suite \ No newline at end of file diff --git a/scripts/delete-application.sh b/scripts/delete-application.sh new file mode 100644 index 0000000..f66cf9e --- /dev/null +++ b/scripts/delete-application.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -H "Content-Type: application/json" -X DELETE http://localhost:8888/application/0 \ No newline at end of file diff --git a/scripts/test-suite.sh b/scripts/test-suite.sh new file mode 100644 index 0000000..3fd0935 --- /dev/null +++ b/scripts/test-suite.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -H "Content-Type: application/json" -X GET http://localhost:8888/application/0/test-suites/0 \ No newline at end of file diff --git a/scripts/test-suites.sh b/scripts/test-suites.sh new file mode 100644 index 0000000..0c45a72 --- /dev/null +++ b/scripts/test-suites.sh @@ -0,0 +1,2 @@ +#!/bin/bash +curl -H "Content-Type: application/json" -X GET http://localhost:8888/application/0/test-suites \ 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 index ccfcef8..dc584a6 100644 --- a/src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala +++ b/src/main/scala/de/retest/guistatemachine/JsonFormatForIdMap.scala @@ -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) }) } } \ 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 f79fc8f..b1f3347 100644 --- a/src/main/scala/de/retest/guistatemachine/RestService.scala +++ b/src/main/scala/de/retest/guistatemachine/RestService.scala @@ -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) @@ -77,5 +76,11 @@ trait RestService { complete(id) } } + } ~ delete { + pathPrefix("application" / LongNumber) { id => + val r = persistence.deleteApplication(Id(id)) + complete(StatusCodes.OK) + } } + } \ 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 index 3d85201..3314403 100644 --- a/src/main/scala/de/retest/guistatemachine/model/GuiApplications.scala +++ b/src/main/scala/de/retest/guistatemachine/model/GuiApplications.scala @@ -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) \ No newline at end of file +final case class GuiApplications(apps: Map[GuiApplication]) \ 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 index 8185acc..705177c 100644 --- a/src/main/scala/de/retest/guistatemachine/model/Map.scala +++ b/src/main/scala/de/retest/guistatemachine/model/Map.scala @@ -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 } \ 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 index 3eb47bd..bc22830 100644 --- a/src/main/scala/de/retest/guistatemachine/model/TestSuites.scala +++ b/src/main/scala/de/retest/guistatemachine/model/TestSuites.scala @@ -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) \ No newline at end of file +final case class TestSuites(suites: Map[TestSuite]) \ 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 index 1617764..54f837e 100644 --- a/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala +++ b/src/main/scala/de/retest/guistatemachine/persistence/Persistence.scala @@ -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 + } } } @@ -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) } } @@ -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 diff --git a/src/test/scala/de/retest/guistatemachine/JsonFormatForIdMapSpec.scala b/src/test/scala/de/retest/guistatemachine/JsonFormatForIdMapSpec.scala new file mode 100644 index 0000000..3bc00dd --- /dev/null +++ b/src/test/scala/de/retest/guistatemachine/JsonFormatForIdMapSpec.scala @@ -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 + } + } + +} \ 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 ef17bd5..aee5c2b 100644 --- a/src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/RestServiceSpec.scala @@ -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 { @@ -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 } } @@ -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 } } @@ -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" + } + } } } diff --git a/src/test/scala/de/retest/guistatemachine/persistence/PersistenceSpec.scala b/src/test/scala/de/retest/guistatemachine/persistence/PersistenceSpec.scala index 3688753..27168a8 100644 --- a/src/test/scala/de/retest/guistatemachine/persistence/PersistenceSpec.scala +++ b/src/test/scala/de/retest/guistatemachine/persistence/PersistenceSpec.scala @@ -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 }