diff options
| -rw-r--r-- | odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt | 6 | ||||
| -rw-r--r-- | odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt | 86 | ||||
| -rw-r--r-- | odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemFactoryTest.kt | 31 | ||||
| -rw-r--r-- | odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemTest.kt | 211 | ||||
| -rw-r--r-- | odcsim-engine-omega/build.gradle | 2 | ||||
| -rw-r--r-- | odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt | 22 | ||||
| -rw-r--r-- | odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactoryTest.kt (renamed from odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/SmokeTest.kt) | 30 | ||||
| -rw-r--r-- | odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt | 36 |
8 files changed, 381 insertions, 43 deletions
diff --git a/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt b/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt index 5a16c4d1..4dea9897 100644 --- a/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt +++ b/odcsim-core/src/main/kotlin/com/atlarge/odcsim/ActorContext.kt @@ -43,11 +43,11 @@ interface ActorContext<in T : Any> { /** * Spawn a child actor from the given [Behavior] and with the specified name. * - * @param name The name of the child actor to spawn. * @param behavior The behavior of the child actor to spawn. + * @param name The name of the child actor to spawn. * @return A reference to the child that has/will be spawned. */ - fun <U : Any> spawn(name: String, behavior: Behavior<U>): ActorRef<U> + fun <U : Any> spawn(behavior: Behavior<U>, name: String): ActorRef<U> /** * Request the specified child actor to be stopped in asynchronous fashion. @@ -55,5 +55,5 @@ interface ActorContext<in T : Any> { * @param child The reference to the child actor to stop. * @return `true` if the ref points to a child actor, otherwise `false`. */ - fun <U : Any> stop(child: ActorRef<U>): Boolean + fun stop(child: ActorRef<*>): Boolean } diff --git a/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt b/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt new file mode 100644 index 00000000..023d3efd --- /dev/null +++ b/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorPathTest.kt @@ -0,0 +1,86 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** + * A test suite for the [ActorPath] class. + */ +@DisplayName("ActorPath") +class ActorPathTest { + /** + * Test whether an [ActorPath.Root] may only start with a slash. + */ + @Test + fun `root node may only start with a slash`() { + ActorPath.Root() // Assert slash at start + assertThrows<IllegalArgumentException> { ActorPath.Root("abc/") } + } + + /** + * Test whether an [ActorPath.Child] disallows names with a slash. + */ + @Test + fun `child node should not allow name with a slash`() { + assertThrows<IllegalArgumentException> { ActorPath.Child(ActorPath.Root(), "/") } + } + + /** + * Test whether a root node can have a custom name. + */ + @Test + fun `root node can have a custom name`() { + val name = "user" + assertEquals(name, ActorPath.Root(name).name) + } + + /** + * Test whether a child node can be created on a root. + */ + @Test + fun `child node can be created on a root`() { + val root = ActorPath.Root(name = "/user") + val child = root.child("child") + + assertEquals(root, child.parent) + assertEquals("child", child.name) + } + + /** + * Test whether a child node can be created on a child. + */ + @Test + fun `child node can be created on a child`() { + val root = ActorPath.Root(name = "/user").child("child") + val child = root.child("child") + + assertEquals(root, child.parent) + assertEquals("child", child.name) + } +} diff --git a/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemFactoryTest.kt b/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemFactoryTest.kt index a374548c..266f9fba 100644 --- a/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemFactoryTest.kt +++ b/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemFactoryTest.kt @@ -24,6 +24,10 @@ package com.atlarge.odcsim +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + /** * A conformance test suite for implementors of the [ActorSystemFactory] interface. */ @@ -32,4 +36,31 @@ abstract class ActorSystemFactoryTest { * Create an [ActorSystemFactory] instance to test. */ abstract fun createFactory(): ActorSystemFactory + + /** + * Test whether the factory will create an [ActorSystem] with correct name. + */ + @Test + fun `should create a system with correct name`() { + val factory = createFactory() + val name = "test" + val system = factory(object : Behavior<Unit> {}, name) + + assertEquals(name, system.name) + } + + /** + * Test whether the factory will create an [ActorSystem] with valid root behavior. + */ + @Test + fun `should create a system with correct root behavior`() { + val factory = createFactory() + val system = factory(object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + throw UnsupportedOperationException() + } + }, "test") + + assertThrows<UnsupportedOperationException> { system.run(until = 10.0) } + } } diff --git a/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemTest.kt b/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemTest.kt index 62df356b..71bc645a 100644 --- a/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemTest.kt +++ b/odcsim-core/src/test/kotlin/com/atlarge/odcsim/ActorSystemTest.kt @@ -25,11 +25,12 @@ package com.atlarge.odcsim import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows -import java.lang.IllegalArgumentException const val DELTA: Double = 0.0001 @@ -54,6 +55,16 @@ abstract class ActorSystemTest { } /** + * Test whether the created [ActorSystem] has a path. + */ + @Test + fun `should have a path`() { + val system = factory(object : Behavior<Unit> {}, "test") + + assertTrue(system.path is ActorPath.Root) + } + + /** * Test whether creating an [ActorSystem] sets the initial time at 0. */ @Test @@ -94,8 +105,202 @@ abstract class ActorSystemTest { val until = 10.0 val system = factory(object : Behavior<Unit> {}, name = "test") - assumeTrue(Math.abs(system.time) < DELTA) system.run(until = until) assertTrue(Math.abs(system.time - until) < DELTA) } + + /** + * Test whether an [ActorSystem] will jump forward in time when asking to run until a specified instant. + */ + @Test + fun `should order messages at the instant by insertion time`() { + val behavior = object : Behavior<Int> { + override fun receive(ctx: ActorContext<Int>, msg: Int): Behavior<Int> { + assertEquals(1, msg) + return object : Behavior<Int> { + override fun receive(ctx: ActorContext<Int>, msg: Int): Behavior<Int> { + assertEquals(2, msg) + return this + } + } + } + } + val system = factory(behavior, name = "test") + system.send(1, after = 1.0) + system.send(2, after = 1.0) + system.run(until = 10.0) + } + + @Nested + @DisplayName("ActorRef") + inner class ActorRefTest { + /** + * Test whether an [ActorSystem] disallows sending messages in the past. + */ + @Test + fun `should disallow messages in the past`() { + val system = factory(object : Behavior<Unit> {}, name = "test") + assertThrows<IllegalArgumentException> { system.send(Unit, after = -1.0) } + } + } + + + @Nested + @DisplayName("Actor") + inner class Actor { + /** + * Test whether the pre-start time of the root actor is at 0. + */ + @Test + fun `should pre-start at t=0 if root`() { + val behavior = object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + assertTrue(Math.abs(ctx.time) < DELTA) + return this + } + } + + val system = factory(behavior, "test") + system.run() + } + + /** + * Test whether a child actor can be created from an actor. + */ + @Test + fun `should allow spawning of child actors`() { + val behavior = object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + throw UnsupportedOperationException("b") + } + } + + val system = factory(object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + if (signal is PreStart) { + val ref = ctx.spawn(behavior, "child") + assertEquals("child", ref.path.name) + } + + return this + } + }, name = "test") + + assertThrows<UnsupportedOperationException> { system.run(until = 10.0) } + } + + /** + * Test whether a child actor can be stopped from an actor. + */ + @Test + fun `should allow stopping of child actors`() { + val system = factory(object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + if (signal is PreStart) { + val ref = ctx.spawn(object : Behavior<Unit> { + override fun receive(ctx: ActorContext<Unit>, msg: Unit): Behavior<Unit> { + throw UnsupportedOperationException() + } + }, "child") + assertTrue(ctx.stop(ref)) + assertEquals("child", ref.path.name) + } + + return this + } + }, name = "test") + + system.run(until = 10.0) + } + + /** + * Test whether only the parent of a child can terminate it. + */ + @Test + fun `should only be able to terminate child actors`() { + val system = factory(object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + val child1 = ctx.spawn(object : Behavior<Unit> {}, "child-1") + ctx.spawn(object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + assertFalse(ctx.stop(child1)) + return this + } + }, "child-2") + return this + } + }, name = "test") + system.run() + } + + /** + * Test whether terminating an already terminated child fails. + */ + @Test + fun `should not be able to stop an already terminated child`() { + val system = factory(object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + val child = ctx.spawn(object : Behavior<Unit> {}, "child") + ctx.stop(child) + assertFalse(ctx.stop(child)) + return this + } + }, name = "test") + system.run() + } + + /** + * Test whether termination of a child also results in termination of its children. + */ + @Test + fun `should terminate children of child when terminating it`() { + val system = factory(object : Behavior<ActorRef<Unit>> { + lateinit var child: ActorRef<Unit> + + override fun receive(ctx: ActorContext<ActorRef<Unit>>, msg: ActorRef<Unit>): Behavior<ActorRef<Unit>> { + assertTrue(ctx.stop(child)) + msg.send(Unit) // This actor should be stopped now and not receive the message anymore + return this + } + + override fun receiveSignal(ctx: ActorContext<ActorRef<Unit>>, signal: Signal): Behavior<ActorRef<Unit>> { + val root = ctx.self + child = ctx.spawn(object : Behavior<Unit> { + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + if (signal is PreStart) { + val child = ctx.spawn(object : Behavior<Unit> { + override fun receive(ctx: ActorContext<Unit>, msg: Unit): Behavior<Unit> { + throw IllegalStateException() + } + }, "child") + root.send(child) + } + return this + } + }, "child") + return this + } + }, name = "test") + system.run() + } + + /** + * Test whether the reference to the actor itself is valid. + */ + @Test + fun `should have reference to itself`() { + val behavior = object : Behavior<Unit> { + override fun receive(ctx: ActorContext<Unit>, msg: Unit): Behavior<Unit> { + throw UnsupportedOperationException() + } + override fun receiveSignal(ctx: ActorContext<Unit>, signal: Signal): Behavior<Unit> { + ctx.self.send(Unit) + return this + } + } + + val system = factory(behavior, "test") + assertThrows<UnsupportedOperationException> { system.run() } + } + } } diff --git a/odcsim-engine-omega/build.gradle b/odcsim-engine-omega/build.gradle index 4f01e4ca..b8399a4e 100644 --- a/odcsim-engine-omega/build.gradle +++ b/odcsim-engine-omega/build.gradle @@ -35,11 +35,9 @@ dependencies { api project(':odcsim-core') implementation "org.jetbrains.kotlin:kotlin-stdlib" - implementation "io.github.microutils:kotlin-logging:1.4.6" testCompile project(path: ':odcsim-core', configuration: 'tests') testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_jupiter_version" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junit_jupiter_version" testImplementation "org.junit.platform:junit-platform-launcher:$junit_platform_version" - testRuntimeOnly "org.slf4j:slf4j-simple:1.7.25" } diff --git a/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt b/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt index ffc68d0b..3eaddf51 100644 --- a/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt +++ b/odcsim-engine-omega/src/main/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystem.kt @@ -34,7 +34,6 @@ import com.atlarge.odcsim.Instant import com.atlarge.odcsim.PostStop import com.atlarge.odcsim.PreStart import com.atlarge.odcsim.Signal -import mu.KotlinLogging import java.util.PriorityQueue import kotlin.math.max @@ -56,7 +55,7 @@ class OmegaActorSystem<in T : Any>(root: Behavior<T>, override val name: String) /** * The path to the root actor. */ - override val path: ActorPath = ActorPath.Root() + override val path: ActorPath = ActorPath.Root(name = "/user") /** * The event queue to process @@ -70,19 +69,16 @@ class OmegaActorSystem<in T : Any>(root: Behavior<T>, override val name: String) */ private val registry: MutableMap<ActorPath, Actor<*>> = HashMap() - private val logger = KotlinLogging.logger {} - override fun run(until: Duration) { - require(until >= .0) { "The given instant must be a positive number" } + require(until >= .0) { "The given instant must be a non-negative number" } while (true) { val envelope = queue.peek() ?: break val delivery = envelope.time.takeUnless { it > until } ?: break - if (delivery < time) { - // Message out of order - logger.warn { "Message delivered out of order [expected=$delivery, actual=$time]" } - } + // A message should never be delivered out of order in this single-threaded implementation. Assert for + // sanity + assert(delivery >= time) { "Message delivered out of order [expected=$delivery, actual=$time]" } time = delivery queue.poll() @@ -114,7 +110,7 @@ class OmegaActorSystem<in T : Any>(root: Behavior<T>, override val name: String) override val time: Instant get() = this@OmegaActorSystem.time - override fun <U : Any> spawn(name: String, behavior: Behavior<U>): ActorRef<U> { + override fun <U : Any> spawn(behavior: Behavior<U>, name: String): ActorRef<U> { val ref = ActorRefImpl<U>(self.path.child(name)) if (ref.path !in registry) { val actor = Actor(ref, behavior) @@ -125,9 +121,9 @@ class OmegaActorSystem<in T : Any>(root: Behavior<T>, override val name: String) return ref } - override fun <U : Any> stop(child: ActorRef<U>): Boolean { - if (child.path.root != this@OmegaActorSystem.path) { - // This child is not part of the hierarchy. + override fun stop(child: ActorRef<*>): Boolean { + if (child.path.parent != self.path) { + // This is not a child of this actor return false } val ref = registry[child.path] ?: return false diff --git a/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/SmokeTest.kt b/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactoryTest.kt index a543d9d1..01574b7f 100644 --- a/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/SmokeTest.kt +++ b/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemFactoryTest.kt @@ -22,30 +22,16 @@ * SOFTWARE. */ -package com.atlarge.odcsim +package com.atlarge.odcsim.engine.omega -import com.atlarge.odcsim.engine.omega.OmegaActorSystem -import org.junit.jupiter.api.Test +import com.atlarge.odcsim.ActorSystemFactory +import com.atlarge.odcsim.ActorSystemFactoryTest +import org.junit.jupiter.api.DisplayName /** - * A test to verify the system runs without smoking. + * The [ActorSystemFactoryTest] suite for the Omega engine implementation. */ -class SmokeTest { - @Test - fun `hello-world`() { - val system = OmegaActorSystem(object : Behavior<String> { - override fun receive(ctx: ActorContext<String>, msg: String): Behavior<String> { - println("${ctx.time} $msg") - return this - } - - override fun receiveSignal(ctx: ActorContext<String>, signal: Signal): Behavior<String> { - println("${ctx.time} $signal") - return this - } - }, name = "test") - - system.send("Hello World", after = 1.2) - system.run() - } +@DisplayName("OmegaActorSystemFactory") +class OmegaActorSystemFactoryTest : ActorSystemFactoryTest() { + override fun createFactory(): ActorSystemFactory = OmegaActorSystemFactory() } diff --git a/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt b/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt new file mode 100644 index 00000000..ef7a5258 --- /dev/null +++ b/odcsim-engine-omega/src/test/kotlin/com/atlarge/odcsim/engine/omega/OmegaActorSystemTest.kt @@ -0,0 +1,36 @@ +/* + * MIT License + * + * Copyright (c) 2018 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 com.atlarge.odcsim.engine.omega + +import com.atlarge.odcsim.ActorSystemTest +import org.junit.jupiter.api.DisplayName + +/** + * The [ActorSystemTest] suite for the Omega engine implementation. + */ +@DisplayName("OmegaActorSystem") +class OmegaActorSystemTest : ActorSystemTest() { + override val factory = OmegaActorSystemFactory() +} |
