diff options
Diffstat (limited to 'opendc-simulator/opendc-simulator-core')
8 files changed, 272 insertions, 226 deletions
diff --git a/opendc-simulator/opendc-simulator-core/build.gradle.kts b/opendc-simulator/opendc-simulator-core/build.gradle.kts index 0de96a8e..0ae95d42 100644 --- a/opendc-simulator/opendc-simulator-core/build.gradle.kts +++ b/opendc-simulator/opendc-simulator-core/build.gradle.kts @@ -28,5 +28,6 @@ plugins { } dependencies { + api(projects.opendc.opendcCommon) api(libs.kotlinx.coroutines) } diff --git a/opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationScheduler.java b/opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationDispatcher.java index 305bdf5e..8c74aacf 100644 --- a/opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationScheduler.java +++ b/opendc-simulator/opendc-simulator-core/src/main/java/org/opendc/simulator/SimulationDispatcher.java @@ -22,17 +22,17 @@ package org.opendc.simulator; -import java.time.Clock; import java.time.Instant; -import java.time.ZoneId; -import java.util.concurrent.Executor; +import java.time.InstantSource; +import org.opendc.common.Dispatcher; +import org.opendc.common.DispatcherHandle; /** - * A scheduler is used by simulations to manage execution of (future) tasks, providing a controllable (virtual) clock to - * skip over delays. + * A {@link Dispatcher} used by simulations to manage execution of (future) tasks, providing a controllable (virtual) + * clock to skip over delays. * * <p> - * The scheduler can be queried to advance the time (via {@link #advanceBy}), run all the scheduled tasks advancing the + * The dispatcher can be queried to advance the time (via {@link #advanceBy}), run all the scheduled tasks advancing the * virtual time as needed (via {@link #advanceUntilIdle}), or run the tasks that are scheduled to run as soon as * possible but have not yet been dispatched (via {@link #runCurrent}). These methods execute the pending tasks using * a single thread. @@ -40,7 +40,7 @@ import java.util.concurrent.Executor; * <p> * This class is not thread-safe and must not be used concurrently by multiple threads. */ -public final class SimulationScheduler implements Executor { +public final class SimulationDispatcher implements Dispatcher { /** * The {@link TaskQueue} containing the pending tasks. */ @@ -57,36 +57,27 @@ public final class SimulationScheduler implements Executor { private int count = 0; /** - * The {@link Clock} instance linked to this scheduler. + * The {@link InstantSource} instance linked to this scheduler. */ - private final SimulationClock clock = new SimulationClock(this, ZoneId.systemDefault()); + private final SimulationClock timeSource = new SimulationClock(this); /** - * Construct a {@link SimulationScheduler} instance with the specified initial time. + * Construct a {@link SimulationDispatcher} instance with the specified initial time. * * @param initialTimeMs The initial virtual time of the scheduler in milliseconds since epoch. */ - public SimulationScheduler(long initialTimeMs) { + public SimulationDispatcher(long initialTimeMs) { this.currentTime = initialTimeMs; } /** - * Construct a {@link SimulationScheduler} instance with the initial time set to UNIX Epoch 0. + * Construct a {@link SimulationDispatcher} instance with the initial time set to UNIX Epoch 0. */ - public SimulationScheduler() { + public SimulationDispatcher() { this(0); } /** - * Return the virtual clock associated with this dispatcher. - * - * @return A {@link Clock} tracking the virtual time of the dispatcher. - */ - public Clock getClock() { - return clock; - } - - /** * Return the current virtual timestamp of the dispatcher (in milliseconds since epoch). * * @return A long value representing the virtual timestamp of the dispatcher in milliseconds since epoch. @@ -96,37 +87,30 @@ public final class SimulationScheduler implements Executor { } /** - * Schedule a <code>task</code> that executes after the specified <code>delayMs</code>. + * Return the virtual time source associated with this dispatcher. * - * @param delayMs The time from now until the execution of the task (in milliseconds). - * @param task The task to execute after the delay. - * @return The identifier of the task that can be used together with the timestamp of the task to cancel it. + * @return A {@link InstantSource} tracking the virtual time of the dispatcher. */ - public int schedule(long delayMs, Runnable task) { - if (delayMs < 0) { - throw new IllegalArgumentException( - "Attempted scheduling an event earlier in time (delay " + delayMs + " ms)"); - } + @Override + public InstantSource getTimeSource() { + return timeSource; + } + @Override + public void schedule(long delayMs, Runnable command) { + internalSchedule(delayMs, command); + } + + @Override + public DispatcherHandle scheduleCancellable(long delayMs, Runnable command) { long target = currentTime + delayMs; if (target < 0) { target = Long.MAX_VALUE; } - int id = count++; - queue.add(target, id, task); - return id; - } - - /** - * Cancel a pending task. - * - * @param deadline The deadline of the task. - * @param id The identifier of the task (returned by {@link #schedule(long, Runnable)}). - * @return A boolean indicating whether a task was actually cancelled. - */ - public boolean cancel(long deadline, int id) { - return queue.remove(deadline, id); + long deadline = target; + int id = internalSchedule(delayMs, command); + return () -> internalCancel(deadline, id); } /** @@ -198,50 +182,62 @@ public final class SimulationScheduler implements Executor { } /** - * Schedule the specified command to run at this moment of virtual time. + * Schedule a <code>task</code> that executes after the specified <code>delayMs</code>. * - * @param command The command to execute. + * @param delayMs The time from now until the execution of the task (in milliseconds). + * @param task The task to execute after the delay. + * @return The identifier of the task that can be used together with the timestamp of the task to cancel it. */ - @Override - public void execute(Runnable command) { - schedule(0, command); + private int internalSchedule(long delayMs, Runnable task) { + if (delayMs < 0) { + throw new IllegalArgumentException( + "Attempted scheduling an event earlier in time (delay " + delayMs + " ms)"); + } + + long target = currentTime + delayMs; + if (target < 0) { + target = Long.MAX_VALUE; + } + + int id = count++; + queue.add(target, id, task); + return id; } /** - * A {@link Clock} implementation for a {@link SimulationScheduler}. + * Cancel a pending task. + * + * @param deadline The deadline of the task. + * @param id The identifier of the task (returned by {@link #internalSchedule(long, Runnable)}). + * @return A boolean indicating whether a task was actually cancelled. */ - private static class SimulationClock extends Clock { - private final SimulationScheduler scheduler; - private final ZoneId zone; - - SimulationClock(SimulationScheduler scheduler, ZoneId zone) { - this.scheduler = scheduler; - this.zone = zone; - } + private boolean internalCancel(long deadline, int id) { + return queue.remove(deadline, id); + } - @Override - public ZoneId getZone() { - return zone; - } + /** + * A {@link InstantSource} implementation for a {@link SimulationDispatcher}. + */ + private static class SimulationClock implements InstantSource { + private final SimulationDispatcher dispatcher; - @Override - public Clock withZone(ZoneId zoneId) { - return new SimulationClock(scheduler, zone); + SimulationClock(SimulationDispatcher dispatcher) { + this.dispatcher = dispatcher; } @Override public Instant instant() { - return Instant.ofEpochMilli(scheduler.currentTime); + return Instant.ofEpochMilli(dispatcher.currentTime); } @Override public long millis() { - return scheduler.currentTime; + return dispatcher.currentTime; } @Override public String toString() { - return "SimulationClock[time=" + millis() + "ms]"; + return "SimulationDispatcher.InstantSource[time=" + millis() + "ms]"; } } } diff --git a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt index 882a0fc5..6e568137 100644 --- a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt +++ b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationBuilders.kt @@ -22,11 +22,18 @@ package org.opendc.simulator.kotlin +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable.children import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async -import org.opendc.simulator.SimulationScheduler +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.opendc.common.DispatcherProvider +import org.opendc.common.asCoroutineDispatcher +import org.opendc.simulator.SimulationDispatcher +import java.time.InstantSource import kotlin.coroutines.ContinuationInterceptor import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -54,16 +61,16 @@ import kotlin.coroutines.EmptyCoroutineContext * The simulation is run in a single thread, unless other [CoroutineDispatcher] are used for child coroutines. * Because of this, child coroutines are not executed in parallel to [body]. * In order for the spawned-off asynchronous code to actually be executed, one must either [yield] or suspend the - * body some other way, or use commands that control scheduling (see [SimulationScheduler]). + * body some other way, or use commands that control scheduling (see [SimulationDispatcher]). */ @OptIn(ExperimentalCoroutinesApi::class) public fun runSimulation( context: CoroutineContext = EmptyCoroutineContext, - scheduler: SimulationScheduler = SimulationScheduler(), + scheduler: SimulationDispatcher = SimulationDispatcher(), body: suspend SimulationCoroutineScope.() -> Unit ) { - val (safeContext, dispatcher) = context.checkArguments(scheduler) - val startingJobs = safeContext.activeJobs() + val (safeContext, job, dispatcher) = context.checkArguments(scheduler) + val startingJobs = job.activeJobs() val scope = SimulationCoroutineScope(safeContext) val deferred = scope.async { body(scope) @@ -72,7 +79,7 @@ public fun runSimulation( deferred.getCompletionExceptionOrNull()?.let { throw it } - val endingJobs = safeContext.activeJobs() + val endingJobs = job.activeJobs() if ((endingJobs - startingJobs).isNotEmpty()) { throw IllegalStateException("Test finished with active jobs: $endingJobs") } @@ -82,24 +89,51 @@ public fun runSimulation( * Convenience method for calling [runSimulation] on an existing [SimulationCoroutineScope]. */ public fun SimulationCoroutineScope.runSimulation(block: suspend SimulationCoroutineScope.() -> Unit): Unit = - runSimulation(coroutineContext, scheduler, block) + runSimulation(coroutineContext, dispatcher, block) + +private fun CoroutineContext.checkArguments(scheduler: SimulationDispatcher): Triple<CoroutineContext, Job, SimulationController> { + val job = get(Job) ?: SupervisorJob() + val dispatcher = get(ContinuationInterceptor) ?: scheduler.asCoroutineDispatcher() + val simulationDispatcher = dispatcher.asSimulationDispatcher() + return Triple(this + dispatcher + job, job, simulationDispatcher.asController()) +} + +private fun Job.activeJobs(): Set<Job> { + return children.filter { it.isActive }.toSet() +} /** - * Convenience method for calling [runSimulation] on an existing [SimulationCoroutineDispatcher]. + * Convert a [ContinuationInterceptor] into a [SimulationDispatcher] if possible. */ -public fun SimulationCoroutineDispatcher.runSimulation(block: suspend SimulationCoroutineScope.() -> Unit): Unit = - runSimulation(this, scheduler, block) - -private fun CoroutineContext.checkArguments(scheduler: SimulationScheduler): Pair<CoroutineContext, SimulationController> { - val dispatcher = get(ContinuationInterceptor).run { - this?.let { require(this is SimulationController) { "Dispatcher must implement SimulationController: $this" } } - this ?: SimulationCoroutineDispatcher(scheduler) - } +internal fun ContinuationInterceptor.asSimulationDispatcher(): SimulationDispatcher { + val provider = this as? DispatcherProvider ?: throw IllegalArgumentException( + "DispatcherProvider such as SimulatorCoroutineDispatcher as the ContinuationInterceptor(Dispatcher) is required" + ) - val job = get(Job) ?: SupervisorJob() - return Pair(this + dispatcher + job, dispatcher as SimulationController) + return provider.dispatcher as? SimulationDispatcher ?: throw IllegalArgumentException("Active dispatcher is not a SimulationDispatcher") } -private fun CoroutineContext.activeJobs(): Set<Job> { - return checkNotNull(this[Job]).children.filter { it.isActive }.toSet() +/** + * Helper method to convert a [SimulationDispatcher] into a [SimulationController]. + */ +internal fun SimulationDispatcher.asController(): SimulationController { + return object : SimulationController { + override val dispatcher: SimulationDispatcher + get() = this@asController + + override val timeSource: InstantSource + get() = this@asController.timeSource + + override fun advanceUntilIdle() { + dispatcher.advanceUntilIdle() + } + + override fun advanceBy(delayMs: Long) { + dispatcher.advanceBy(delayMs) + } + + override fun runCurrent() { + dispatcher.runCurrent() + } + } } diff --git a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt index f96b2326..f7470ad9 100644 --- a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt +++ b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationController.kt @@ -23,30 +23,48 @@ package org.opendc.simulator.kotlin import kotlinx.coroutines.CoroutineDispatcher -import org.opendc.simulator.SimulationScheduler -import java.time.Clock +import org.opendc.simulator.SimulationDispatcher +import java.time.InstantSource /** - * Control the virtual clock of a [CoroutineDispatcher]. + * Interface to control the virtual clock of a [CoroutineDispatcher]. */ public interface SimulationController { /** * The current virtual clock as it is known to this Dispatcher. */ - public val clock: Clock + public val timeSource: InstantSource /** - * The [SimulationScheduler] driving the simulation. + * The current virtual timestamp of the dispatcher (in milliseconds since epoch). */ - public val scheduler: SimulationScheduler + public val currentTime: Long + get() = timeSource.millis() + + /** + * Return the [SimulationDispatcher] driving the simulation. + */ + public val dispatcher: SimulationDispatcher /** * Immediately execute all pending tasks and advance the virtual clock-time to the last delay. * * If new tasks are scheduled due to advancing virtual time, they will be executed before `advanceUntilIdle` * returns. + */ + public fun advanceUntilIdle() + + /** + * Move the virtual clock of this dispatcher forward by the specified amount, running the scheduled tasks in the + * meantime. * - * @return the amount of delay-time that this Dispatcher's clock has been forwarded in milliseconds. + * @param delayMs The amount of time to move the virtual clock forward (in milliseconds). + * @throws IllegalStateException if passed a negative <code>delay</code>. + */ + public fun advanceBy(delayMs: Long) + + /** + * Execute the tasks that are scheduled to execute at this moment of virtual time. */ - public fun advanceUntilIdle(): Long + public fun runCurrent() } diff --git a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineDispatcher.kt b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineDispatcher.kt deleted file mode 100644 index cacbbbf7..00000000 --- a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineDispatcher.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.simulator.kotlin - -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Delay -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.InternalCoroutinesApi -import org.opendc.simulator.SimulationScheduler -import java.lang.Runnable -import java.time.Clock -import kotlin.coroutines.CoroutineContext - -/** - * A [CoroutineDispatcher] that performs both immediate execution of coroutines on the main thread and uses a virtual - * clock for time management. - * - * @param scheduler The [SimulationScheduler] used to manage the execution of future tasks. - */ -@OptIn(InternalCoroutinesApi::class) -public class SimulationCoroutineDispatcher( - override val scheduler: SimulationScheduler = SimulationScheduler() -) : CoroutineDispatcher(), SimulationController, Delay { - /** - * The virtual clock of this dispatcher. - */ - override val clock: Clock = scheduler.clock - - override fun dispatch(context: CoroutineContext, block: Runnable) { - block.run() - } - - override fun dispatchYield(context: CoroutineContext, block: Runnable) { - scheduler.execute(block) - } - - @OptIn(ExperimentalCoroutinesApi::class) - override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) { - scheduler.schedule(timeMillis, CancellableContinuationRunnable(continuation) { resumeUndispatched(Unit) }) - } - - override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { - return object : DisposableHandle { - private val deadline = (scheduler.currentTime + timeMillis).let { if (it >= 0) it else Long.MAX_VALUE } - private val id = scheduler.schedule(timeMillis, block) - - override fun dispose() { - scheduler.cancel(deadline, id) - } - } - } - - override fun toString(): String { - return "SimulationCoroutineDispatcher[time=${scheduler.currentTime}ms]" - } - - override fun advanceUntilIdle(): Long { - val scheduler = scheduler - val oldTime = scheduler.currentTime - - scheduler.advanceUntilIdle() - - return scheduler.currentTime - oldTime - } - - /** - * This class exists to allow cleanup code to avoid throwing for cancelled continuations scheduled - * in the future. - */ - private class CancellableContinuationRunnable<T>( - @JvmField val continuation: CancellableContinuation<T>, - private val block: CancellableContinuation<T>.() -> Unit - ) : Runnable { - override fun run() = continuation.block() - } -} diff --git a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt index 6be8e67a..ca49fc53 100644 --- a/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt +++ b/opendc-simulator/opendc-simulator-core/src/main/kotlin/org/opendc/simulator/kotlin/SimulationCoroutineScope.kt @@ -24,7 +24,8 @@ package org.opendc.simulator.kotlin import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope -import org.opendc.simulator.SimulationScheduler +import org.opendc.common.asCoroutineDispatcher +import org.opendc.simulator.SimulationDispatcher import kotlin.coroutines.ContinuationInterceptor import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -34,33 +35,28 @@ import kotlin.coroutines.EmptyCoroutineContext */ public interface SimulationCoroutineScope : CoroutineScope, SimulationController -private class SimulationCoroutineScopeImpl( - override val coroutineContext: CoroutineContext -) : - SimulationCoroutineScope, - SimulationController by coroutineContext.simulationController - /** * A scope which provides detailed control over the execution of coroutines for simulations. * * If the provided context does not provide a [ContinuationInterceptor] (Dispatcher) or [CoroutineExceptionHandler], the - * scope adds [SimulationCoroutineDispatcher] automatically. + * scope adds a dispatcher automatically. */ -@Suppress("FunctionName") public fun SimulationCoroutineScope( context: CoroutineContext = EmptyCoroutineContext, - scheduler: SimulationScheduler = SimulationScheduler() + scheduler: SimulationDispatcher = SimulationDispatcher() ): SimulationCoroutineScope { var safeContext = context - if (context[ContinuationInterceptor] == null) safeContext += SimulationCoroutineDispatcher(scheduler) - return SimulationCoroutineScopeImpl(safeContext) -} + val simulationDispatcher: SimulationDispatcher + val interceptor = context[ContinuationInterceptor] -private inline val CoroutineContext.simulationController: SimulationController - get() { - val handler = this[ContinuationInterceptor] - return handler as? SimulationController ?: throw IllegalArgumentException( - "SimulationCoroutineScope requires a SimulationController such as SimulatorCoroutineDispatcher as " + - "the ContinuationInterceptor (Dispatcher)" - ) + if (interceptor != null) { + simulationDispatcher = interceptor.asSimulationDispatcher() + } else { + simulationDispatcher = scheduler + safeContext += scheduler.asCoroutineDispatcher() } + + return object : SimulationCoroutineScope, SimulationController by simulationDispatcher.asController() { + override val coroutineContext: CoroutineContext = safeContext + } +} diff --git a/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationSchedulerTest.kt b/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationDispatcherTest.kt index eca3b582..600102be 100644 --- a/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationSchedulerTest.kt +++ b/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/SimulationDispatcherTest.kt @@ -28,15 +28,15 @@ import org.junit.jupiter.api.assertThrows import java.time.Instant /** - * Test suite for the [SimulationScheduler] class. + * Test suite for the [SimulationDispatcher] class. */ -class SimulationSchedulerTest { +class SimulationDispatcherTest { /** - * Test the basic functionality of [SimulationScheduler.runCurrent]. + * Test the basic functionality of [SimulationDispatcher.runCurrent]. */ @Test fun testRunCurrent() { - val scheduler = SimulationScheduler() + val scheduler = SimulationDispatcher() var count = 0 scheduler.schedule(1) { count += 1 } @@ -58,11 +58,11 @@ class SimulationSchedulerTest { } /** - * Test the clock of the [SimulationScheduler]. + * Test the clock of the [SimulationDispatcher]. */ @Test fun testClock() { - val scheduler = SimulationScheduler() + val scheduler = SimulationDispatcher() var count = 0 scheduler.schedule(1) { count += 1 } @@ -70,8 +70,8 @@ class SimulationSchedulerTest { scheduler.advanceBy(2) assertEquals(2, scheduler.currentTime) - assertEquals(2, scheduler.clock.millis()) - assertEquals(Instant.ofEpochMilli(2), scheduler.clock.instant()) + assertEquals(2, scheduler.timeSource.millis()) + assertEquals(Instant.ofEpochMilli(2), scheduler.timeSource.instant()) } /** @@ -79,7 +79,7 @@ class SimulationSchedulerTest { */ @Test fun testAdvanceByLargeDelays() { - val scheduler = SimulationScheduler() + val scheduler = SimulationDispatcher() var count = 0 scheduler.schedule(1) { count += 1 } @@ -87,10 +87,11 @@ class SimulationSchedulerTest { scheduler.advanceBy(10) scheduler.schedule(Long.MAX_VALUE) { count += 1 } + scheduler.scheduleCancellable(Long.MAX_VALUE) { count += 1 } scheduler.schedule(100_000_000) { count += 1 } scheduler.advanceUntilIdle() - assertEquals(3, count) + assertEquals(4, count) } /** @@ -98,7 +99,7 @@ class SimulationSchedulerTest { */ @Test fun testNegativeDelays() { - val scheduler = SimulationScheduler() + val scheduler = SimulationDispatcher() assertThrows<IllegalArgumentException> { scheduler.schedule(-100) { } } assertThrows<IllegalArgumentException> { scheduler.advanceBy(-100) } diff --git a/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/kotlin/SimulationBuildersTest.kt b/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/kotlin/SimulationBuildersTest.kt new file mode 100644 index 00000000..26419a50 --- /dev/null +++ b/opendc-simulator/opendc-simulator-core/src/test/kotlin/org/opendc/simulator/kotlin/SimulationBuildersTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2022 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.simulator.kotlin + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +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.Test +import org.junit.jupiter.api.assertThrows + +/** + * Test suite for the Kotlin simulation builders. + */ +class SimulationBuildersTest { + @Test + fun testDelay() = runSimulation { + assertEquals(0, currentTime) + delay(100) + assertEquals(100, currentTime) + } + + @Test + fun testController() = runSimulation { + var completed = false + + launch { + delay(20) + completed = true + } + + advanceBy(10) + assertFalse(completed) + advanceBy(11) + assertTrue(completed) + + completed = false + launch { completed = true } + runCurrent() + assertTrue(completed) + } + + @Test + fun testFailOnActiveJobs() { + assertThrows<IllegalStateException> { + runSimulation { + launch { suspendCancellableCoroutine {} } + } + } + } + + @Test + fun testPropagateException() { + assertThrows<IllegalStateException> { + runSimulation { + throw IllegalStateException("Test") + } + } + } + + @Test + fun testInvalidDispatcher() { + assertThrows<IllegalArgumentException> { + runSimulation(Dispatchers.Default) { } + } + } + + @Test + fun testExistingJob() { + runSimulation(Job()) { + delay(10) + } + } +} |
