summaryrefslogtreecommitdiff
path: root/opendc-compute/opendc-compute-service/src/test
diff options
context:
space:
mode:
authorFabian Mastenbroek <mail.fabianm@gmail.com>2021-04-25 21:53:42 +0200
committerGitHub <noreply@github.com>2021-04-25 21:53:42 +0200
commit128f76f7fd7c8abb41a3bbbd9f1980cbc20ae7a5 (patch)
treeadd513890005233a7784466797bfe6f5052e9eeb /opendc-compute/opendc-compute-service/src/test
parent128a1db017545597a5c035b7960eb3fd36b5f987 (diff)
parent57b54b59ed74ec37338ae26b3864d051255aba49 (diff)
build: Flatten project structure
This change updates the project structure to become flattened. Previously, the simulator, frontend and API each lived into their own directory. With this change, all modules of the project live in the top-level directory of the repository.
Diffstat (limited to 'opendc-compute/opendc-compute-service/src/test')
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt391
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt80
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt81
-rw-r--r--opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt285
-rw-r--r--opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml38
5 files changed, 875 insertions, 0 deletions
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt
new file mode 100644
index 00000000..a6258845
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/ComputeServiceTest.kt
@@ -0,0 +1,391 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.compute.service
+
+import io.mockk.*
+import io.opentelemetry.api.metrics.MeterProvider
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.opendc.compute.api.*
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.driver.HostListener
+import org.opendc.compute.service.driver.HostModel
+import org.opendc.compute.service.driver.HostState
+import org.opendc.compute.service.scheduler.FilterScheduler
+import org.opendc.compute.service.scheduler.filters.ComputeCapabilitiesFilter
+import org.opendc.compute.service.scheduler.filters.ComputeFilter
+import org.opendc.compute.service.scheduler.weights.MemoryWeigher
+import org.opendc.simulator.core.SimulationCoroutineScope
+import org.opendc.simulator.core.runBlockingSimulation
+import java.util.*
+
+/**
+ * Test suite for the [ComputeService] interface.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class ComputeServiceTest {
+ lateinit var scope: SimulationCoroutineScope
+ lateinit var service: ComputeService
+
+ @BeforeEach
+ fun setUp() {
+ scope = SimulationCoroutineScope()
+ val clock = scope.clock
+ val computeScheduler = FilterScheduler(
+ filters = listOf(ComputeFilter(), ComputeCapabilitiesFilter()),
+ weighers = listOf(MemoryWeigher() to -1.0)
+ )
+ val meter = MeterProvider.noop().get("opendc-compute")
+ service = ComputeService(scope.coroutineContext, clock, meter, computeScheduler)
+ }
+
+ @Test
+ fun testClientClose() = scope.runBlockingSimulation {
+ val client = service.newClient()
+
+ assertEquals(emptyList<Flavor>(), client.queryFlavors())
+ assertEquals(emptyList<Image>(), client.queryImages())
+ assertEquals(emptyList<Server>(), client.queryServers())
+
+ client.close()
+
+ assertThrows<IllegalStateException> { client.queryFlavors() }
+ assertThrows<IllegalStateException> { client.queryImages() }
+ assertThrows<IllegalStateException> { client.queryServers() }
+
+ assertThrows<IllegalStateException> { client.findFlavor(UUID.randomUUID()) }
+ assertThrows<IllegalStateException> { client.findImage(UUID.randomUUID()) }
+ assertThrows<IllegalStateException> { client.findServer(UUID.randomUUID()) }
+
+ assertThrows<IllegalStateException> { client.newFlavor("test", 1, 2) }
+ assertThrows<IllegalStateException> { client.newImage("test") }
+ assertThrows<IllegalStateException> { client.newServer("test", mockk(), mockk()) }
+ }
+
+ @Test
+ fun testClientCreate() = scope.runBlockingSimulation {
+ val client = service.newClient()
+
+ val flavor = client.newFlavor("test", 1, 1024)
+ assertEquals(listOf(flavor), client.queryFlavors())
+ assertEquals(flavor, client.findFlavor(flavor.uid))
+ val image = client.newImage("test")
+ assertEquals(listOf(image), client.queryImages())
+ assertEquals(image, client.findImage(image.uid))
+ val server = client.newServer("test", image, flavor, start = false)
+ assertEquals(listOf(server), client.queryServers())
+ assertEquals(server, client.findServer(server.uid))
+
+ server.delete()
+ assertNull(client.findServer(server.uid))
+
+ image.delete()
+ assertNull(client.findImage(image.uid))
+
+ flavor.delete()
+ assertNull(client.findFlavor(flavor.uid))
+
+ assertThrows<IllegalStateException> { server.start() }
+ }
+
+ @Test
+ fun testClientOnClose() = scope.runBlockingSimulation {
+ service.close()
+ assertThrows<IllegalStateException> {
+ service.newClient()
+ }
+ }
+
+ @Test
+ fun testAddHost() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+
+ assertEquals(0, service.hostCount)
+ assertEquals(emptySet<Host>(), service.hosts)
+
+ service.addHost(host)
+
+ verify(exactly = 1) { host.addListener(any()) }
+
+ assertEquals(1, service.hostCount)
+ assertEquals(1, service.hosts.size)
+
+ service.removeHost(host)
+
+ verify(exactly = 1) { host.removeListener(any()) }
+ }
+
+ @Test
+ fun testAddHostDouble() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.DOWN
+
+ assertEquals(0, service.hostCount)
+ assertEquals(emptySet<Host>(), service.hosts)
+
+ service.addHost(host)
+ service.addHost(host)
+
+ verify(exactly = 1) { host.addListener(any()) }
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughCpus() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 0)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughMemory() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 0, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerStartWithoutEnoughResources() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.ERROR, server.state)
+ }
+
+ @Test
+ fun testServerCancelRequest() = scope.runBlockingSimulation {
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ server.stop()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testServerCannotFitOnHost() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(10 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify { host.canFit(server) }
+ }
+
+ @Test
+ fun testHostAvailableAfterSomeTime() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.DOWN
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+
+ every { host.state } returns HostState.UP
+ listeners.forEach { it.onStateChanged(host, HostState.UP) }
+
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify { host.canFit(server) }
+ }
+
+ @Test
+ fun testHostUnavailableAfterSomeTime() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ every { host.canFit(any()) } returns false
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ delay(5 * 60 * 1000)
+
+ every { host.state } returns HostState.DOWN
+ listeners.forEach { it.onStateChanged(host, HostState.DOWN) }
+
+ server.start()
+ delay(5 * 60 * 1000)
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+
+ verify(exactly = 0) { host.canFit(server) }
+ }
+
+ @Test
+ fun testServerInvalidType() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ assertThrows<IllegalArgumentException> {
+ listeners.forEach { it.onStateChanged(host, server, ServerState.RUNNING) }
+ }
+ }
+
+ @Test
+ fun testServerDeploy() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+ val slot = slot<Server>()
+
+ val watcher = mockk<ServerWatcher>(relaxUnitFun = true)
+ server.watch(watcher)
+
+ // Start server
+ server.start()
+ delay(5 * 60 * 1000)
+ coVerify { host.spawn(capture(slot), true) }
+
+ listeners.forEach { it.onStateChanged(host, slot.captured, ServerState.RUNNING) }
+
+ server.refresh()
+ assertEquals(ServerState.RUNNING, server.state)
+
+ verify { watcher.onStateChanged(server, ServerState.RUNNING) }
+
+ // Stop server
+ listeners.forEach { it.onStateChanged(host, slot.captured, ServerState.TERMINATED) }
+
+ server.refresh()
+ assertEquals(ServerState.TERMINATED, server.state)
+
+ verify { watcher.onStateChanged(server, ServerState.TERMINATED) }
+ }
+
+ @Test
+ fun testServerDeployFailure() = scope.runBlockingSimulation {
+ val host = mockk<Host>(relaxUnitFun = true)
+ val listeners = mutableListOf<HostListener>()
+
+ every { host.uid } returns UUID.randomUUID()
+ every { host.model } returns HostModel(4, 2048)
+ every { host.state } returns HostState.UP
+ every { host.canFit(any()) } returns true
+ every { host.addListener(any()) } answers { listeners.add(it.invocation.args[0] as HostListener) }
+ coEvery { host.spawn(any(), true) } throws IllegalStateException()
+
+ service.addHost(host)
+
+ val client = service.newClient()
+ val flavor = client.newFlavor("test", 1, 1024)
+ val image = client.newImage("test")
+ val server = client.newServer("test", image, flavor, start = false)
+
+ server.start()
+ delay(5 * 60 * 1000)
+
+ server.refresh()
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt
new file mode 100644
index 00000000..18d698c6
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalFlavorTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.compute.service
+
+import io.mockk.*
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Test
+import org.opendc.compute.api.Flavor
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import java.util.*
+
+/**
+ * Test suite for the [InternalFlavor] implementation.
+ */
+class InternalFlavorTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+ val b = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Flavor>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Flavor>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalFlavor(service, uid, "test", 1, 1024, mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt
new file mode 100644
index 00000000..e1cb0128
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalImageTest.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.compute.service
+
+import io.mockk.*
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Test
+import org.opendc.compute.api.Image
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import org.opendc.compute.service.internal.InternalImage
+import java.util.*
+
+/**
+ * Test suite for the [InternalFlavor] implementation.
+ */
+class InternalImageTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+ val b = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Image>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Image>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val a = InternalImage(service, uid, "test", mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt
new file mode 100644
index 00000000..20ea8d20
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/kotlin/org/opendc/compute/service/InternalServerTest.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright (c) 2021 AtLarge Research
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package org.opendc.compute.service
+
+import io.mockk.*
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.yield
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import org.opendc.compute.api.Server
+import org.opendc.compute.api.ServerState
+import org.opendc.compute.service.driver.Host
+import org.opendc.compute.service.internal.ComputeServiceImpl
+import org.opendc.compute.service.internal.InternalFlavor
+import org.opendc.compute.service.internal.InternalImage
+import org.opendc.compute.service.internal.InternalServer
+import org.opendc.simulator.core.runBlockingSimulation
+import java.util.*
+
+/**
+ * Test suite for the [InternalServer] implementation.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+class InternalServerTest {
+ @Test
+ fun testEquality() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val b = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testEqualityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Server>(relaxUnitFun = true)
+ every { b.uid } returns uid
+
+ assertEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithDifferentType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ val b = mockk<Server>(relaxUnitFun = true)
+ every { b.uid } returns UUID.randomUUID()
+
+ assertNotEquals(a, b)
+ }
+
+ @Test
+ fun testInequalityWithIncorrectType() {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val a = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ assertNotEquals(a, Unit)
+ }
+
+ @Test
+ fun testStartTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ every { service.schedule(any()) } answers { ComputeServiceImpl.SchedulingRequest(it.invocation.args[0] as InternalServer) }
+
+ server.start()
+
+ verify(exactly = 1) { service.schedule(server) }
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+
+ @Test
+ fun testStartDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+
+ assertThrows<IllegalStateException> { server.start() }
+ }
+
+ @Test
+ fun testStartProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.PROVISIONING
+
+ server.start()
+
+ assertEquals(ServerState.PROVISIONING, server.state)
+ }
+
+ @Test
+ fun testStartRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.RUNNING
+
+ server.start()
+
+ assertEquals(ServerState.RUNNING, server.state)
+ }
+
+ @Test
+ fun testStopProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val request = ComputeServiceImpl.SchedulingRequest(server)
+
+ every { service.schedule(any()) } returns request
+
+ server.start()
+ server.stop()
+
+ assertTrue(request.isCancelled)
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testStopTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.TERMINATED
+ server.stop()
+
+ assertEquals(ServerState.TERMINATED, server.state)
+ }
+
+ @Test
+ fun testStopDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+ server.stop()
+
+ assertEquals(ServerState.DELETED, server.state)
+ }
+
+ @Test
+ fun testStopRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>()
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ server.state = ServerState.RUNNING
+ server.host = host
+ server.stop()
+ yield()
+
+ coVerify { host.stop(server) }
+ }
+
+ @Test
+ fun testDeleteProvisioningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val request = ComputeServiceImpl.SchedulingRequest(server)
+
+ every { service.schedule(any()) } returns request
+
+ server.start()
+ server.delete()
+
+ assertTrue(request.isCancelled)
+ assertEquals(ServerState.DELETED, server.state)
+ verify { service.delete(server) }
+ }
+
+ @Test
+ fun testDeleteTerminatedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.TERMINATED
+ server.delete()
+
+ assertEquals(ServerState.DELETED, server.state)
+
+ verify { service.delete(server) }
+ }
+
+ @Test
+ fun testDeleteDeletedServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+
+ server.state = ServerState.DELETED
+ server.delete()
+
+ assertEquals(ServerState.DELETED, server.state)
+ }
+
+ @Test
+ fun testDeleteRunningServer() = runBlockingSimulation {
+ val service = mockk<ComputeServiceImpl>(relaxUnitFun = true)
+ val uid = UUID.randomUUID()
+ val flavor = mockk<InternalFlavor>()
+ val image = mockk<InternalImage>()
+ val server = InternalServer(service, uid, "test", flavor, image, mutableMapOf(), mutableMapOf())
+ val host = mockk<Host>(relaxUnitFun = true)
+
+ server.state = ServerState.RUNNING
+ server.host = host
+ server.delete()
+ yield()
+
+ coVerify { host.delete(server) }
+ verify { service.delete(server) }
+ }
+}
diff --git a/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml b/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml
new file mode 100644
index 00000000..0dfb75f2
--- /dev/null
+++ b/opendc-compute/opendc-compute-service/src/test/resources/log4j2.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Copyright (c) 2021 AtLarge Research
+ ~
+ ~ Permission is hereby granted, free of charge, to any person obtaining a copy
+ ~ of this software and associated documentation files (the "Software"), to deal
+ ~ in the Software without restriction, including without limitation the rights
+ ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ ~ copies of the Software, and to permit persons to whom the Software is
+ ~ furnished to do so, subject to the following conditions:
+ ~
+ ~ The above copyright notice and this permission notice shall be included in all
+ ~ copies or substantial portions of the Software.
+ ~
+ ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ ~ SOFTWARE.
+ -->
+
+<Configuration status="WARN" packages="org.apache.logging.log4j.core">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_OUT">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%highlight{%-5level}] %logger{36} - %msg%n" disableAnsi="false"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Logger name="org.opendc" level="trace" additivity="false">
+ <AppenderRef ref="Console"/>
+ </Logger>
+ <Root level="info">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>