diff options
Diffstat (limited to 'opendc-simulator/opendc-simulator-power')
9 files changed, 765 insertions, 0 deletions
diff --git a/opendc-simulator/opendc-simulator-power/build.gradle.kts b/opendc-simulator/opendc-simulator-power/build.gradle.kts new file mode 100644 index 00000000..5d8c8949 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +description = "Library for simulating datacenter power components" + +plugins { + `kotlin-library-conventions` + `testing-conventions` + `jacoco-conventions` +} + +dependencies { + api(platform(projects.opendcPlatform)) + api(projects.opendcSimulator.opendcSimulatorFlow) + implementation(projects.opendcSimulator.opendcSimulatorCore) + + testImplementation(libs.slf4j.simple) +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt new file mode 100644 index 00000000..9f88fecc --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPdu.kt @@ -0,0 +1,92 @@ +/* + * 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.power + +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.mux.FlowMultiplexer +import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer + +/** + * A model of a Power Distribution Unit (PDU). + * + * @param engine The underlying [FlowEngine] to drive the simulation under the hood. + * @param idlePower The idle power consumption of the PDU independent of the load on the PDU. + * @param lossCoefficient The coefficient for the power loss of the PDU proportional to the square load. + */ +public class SimPdu( + engine: FlowEngine, + private val idlePower: Double = 0.0, + private val lossCoefficient: Double = 0.0, +) : SimPowerInlet() { + /** + * The [FlowMultiplexer] that distributes the electricity over the PDU outlets. + */ + private val mux = MaxMinFlowMultiplexer(engine) + + /** + * The [FlowForwarder] that represents the input of the PDU. + */ + private val output = mux.newOutput() + + /** + * Create a new PDU outlet. + */ + public fun newOutlet(): Outlet = Outlet(mux, mux.newInput()) + + override fun createSource(): FlowSource = FlowMapper(output) { _, rate -> + val loss = computePowerLoss(rate) + rate + loss + } + + override fun toString(): String = "SimPdu" + + /** + * Compute the power loss that occurs in the PDU. + */ + private fun computePowerLoss(load: Double): Double { + // See https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN + return idlePower + lossCoefficient * (load * load) + } + + /** + * A PDU outlet. + */ + public class Outlet(private val switch: FlowMultiplexer, private val provider: FlowConsumer) : SimPowerOutlet(), AutoCloseable { + override fun onConnect(inlet: SimPowerInlet) { + provider.startConsumer(inlet.createSource()) + } + + override fun onDisconnect(inlet: SimPowerInlet) { + provider.cancel() + } + + /** + * Remove the outlet from the PDU. + */ + override fun close() { + switch.removeInput(provider) + } + + override fun toString(): String = "SimPdu.Outlet" + } +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt new file mode 100644 index 00000000..de587b7f --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerInlet.kt @@ -0,0 +1,48 @@ +/* + * 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.power + +import org.opendc.simulator.flow.FlowSource + +/** + * An abstract inlet that consumes electricity from a power outlet. + */ +public abstract class SimPowerInlet { + /** + * A flag to indicate that the inlet is currently connected to an outlet. + */ + public val isConnected: Boolean + get() = _outlet != null + + /** + * The [SimPowerOutlet] to which the inlet is connected. + */ + public val outlet: SimPowerOutlet? + get() = _outlet + internal var _outlet: SimPowerOutlet? = null + + /** + * Create a [FlowSource] which represents the consumption of electricity from the power outlet. + */ + public abstract fun createSource(): FlowSource +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerOutlet.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerOutlet.kt new file mode 100644 index 00000000..72f52acc --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerOutlet.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.simulator.power + +/** + * An abstract outlet that provides a source of electricity for datacenter components. + */ +public abstract class SimPowerOutlet { + /** + * A flag to indicate that the inlet is currently connected to an outlet. + */ + public val isConnected: Boolean + get() = _inlet != null + + /** + * The inlet that is connected to this outlet currently. + */ + public val inlet: SimPowerInlet? + get() = _inlet + private var _inlet: SimPowerInlet? = null + + /** + * Connect the specified power [inlet] to this outlet. + * + * @param inlet The inlet to connect to the outlet. + */ + public fun connect(inlet: SimPowerInlet) { + check(!isConnected) { "Outlet already connected" } + check(!inlet.isConnected) { "Inlet already connected" } + + _inlet = inlet + inlet._outlet = this + + onConnect(inlet) + } + + /** + * Disconnect the connected power outlet from this inlet + */ + public fun disconnect() { + val inlet = _inlet + if (inlet != null) { + _inlet = null + assert(inlet._outlet == this) { "Inlet state incorrect" } + inlet._outlet = null + + onDisconnect(inlet) + } + } + + /** + * This method is invoked when an inlet is connected to the outlet. + */ + protected abstract fun onConnect(inlet: SimPowerInlet) + + /** + * This method is invoked when an inlet is disconnected from the outlet. + */ + protected abstract fun onDisconnect(inlet: SimPowerInlet) +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt new file mode 100644 index 00000000..07e9f52e --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimPowerSource.kt @@ -0,0 +1,54 @@ +/* + * 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.power + +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSink + +/** + * A [SimPowerOutlet] that represents a source of electricity. + * + * @param engine The underlying [FlowEngine] to drive the simulation under the hood. + */ +public class SimPowerSource(engine: FlowEngine, public val capacity: Double) : SimPowerOutlet() { + /** + * The resource source that drives this power source. + */ + private val source = FlowSink(engine, capacity) + + /** + * The power draw at this instant. + */ + public val powerDraw: Double + get() = source.rate + + override fun onConnect(inlet: SimPowerInlet) { + source.startConsumer(inlet.createSource()) + } + + override fun onDisconnect(inlet: SimPowerInlet) { + source.cancel() + } + + override fun toString(): String = "SimPowerSource" +} diff --git a/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt new file mode 100644 index 00000000..46d659f8 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/main/kotlin/org/opendc/simulator/power/SimUps.kt @@ -0,0 +1,98 @@ +/* + * 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.power + +import org.opendc.simulator.flow.* +import org.opendc.simulator.flow.mux.MaxMinFlowMultiplexer + +/** + * A model of an Uninterruptible Power Supply (UPS). + * + * This model aggregates multiple power sources into a single source in order to ensure that power is always available. + * + * @param engine The underlying [FlowEngine] to drive the simulation under the hood. + * @param idlePower The idle power consumption of the UPS independent of the load. + * @param lossCoefficient The coefficient for the power loss of the UPS proportional to the load. + */ +public class SimUps( + private val engine: FlowEngine, + private val idlePower: Double = 0.0, + private val lossCoefficient: Double = 0.0, +) : SimPowerOutlet() { + /** + * The resource aggregator used to combine the input sources. + */ + private val mux = MaxMinFlowMultiplexer(engine) + + /** + * The [FlowConsumer] that represents the output of the UPS. + */ + private val provider = mux.newInput() + + /** + * Create a new UPS outlet. + */ + public fun newInlet(): SimPowerInlet { + val forward = FlowForwarder(engine, isCoupled = true) + forward.startConsumer(mux.newOutput()) + return Inlet(forward) + } + + override fun onConnect(inlet: SimPowerInlet) { + val source = inlet.createSource() + val mapper = FlowMapper(source) { _, rate -> + val loss = computePowerLoss(rate) + rate + loss + } + + provider.startConsumer(mapper) + } + + override fun onDisconnect(inlet: SimPowerInlet) { + provider.cancel() + } + + /** + * Compute the power loss that occurs in the UPS. + */ + private fun computePowerLoss(load: Double): Double { + // See https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN + return idlePower + lossCoefficient * load + } + + /** + * A UPS inlet. + */ + public inner class Inlet(private val forwarder: FlowForwarder) : SimPowerInlet(), AutoCloseable { + override fun createSource(): FlowSource = forwarder + + /** + * Remove the inlet from the PSU. + */ + override fun close() { + forwarder.close() + } + + override fun toString(): String = "SimPsu.Inlet" + } +} diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt new file mode 100644 index 00000000..eb823eb1 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPduTest.kt @@ -0,0 +1,119 @@ +/* + * 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.power + +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSource +import org.opendc.simulator.flow.source.FixedFlowSource + +/** + * Test suite for the [SimPdu] class. + */ +internal class SimPduTest { + @Test + fun testZeroOutlets() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) + source.connect(pdu) + + assertEquals(0.0, source.powerDraw) + } + + @Test + fun testSingleOutlet() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) + source.connect(pdu) + pdu.newOutlet().connect(SimpleInlet()) + + assertEquals(50.0, source.powerDraw) + } + + @Test + fun testDoubleOutlet() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) + source.connect(pdu) + + pdu.newOutlet().connect(SimpleInlet()) + pdu.newOutlet().connect(SimpleInlet()) + + assertEquals(100.0, source.powerDraw) + } + + @Test + fun testDisconnect() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) + source.connect(pdu) + val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) + val inlet = object : SimPowerInlet() { + override fun createSource(): FlowSource = consumer + } + + val outlet = pdu.newOutlet() + outlet.connect(inlet) + outlet.disconnect() + + verify { consumer.onStop(any(), any(), any()) } + } + + @Test + fun testLoss() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + // https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN + val pdu = SimPdu(engine, idlePower = 1.5, lossCoefficient = 0.015) + source.connect(pdu) + pdu.newOutlet().connect(SimpleInlet()) + assertEquals(89.0, source.powerDraw, 0.01) + } + + @Test + fun testOutletClose() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val pdu = SimPdu(engine) + source.connect(pdu) + val outlet = pdu.newOutlet() + outlet.close() + + assertThrows<IllegalStateException> { + outlet.connect(SimpleInlet()) + } + } + + class SimpleInlet : SimPowerInlet() { + override fun createSource(): FlowSource = FixedFlowSource(100.0, utilization = 0.5) + } +} diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt new file mode 100644 index 00000000..76142103 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimPowerSourceTest.kt @@ -0,0 +1,136 @@ +/* + * 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.power + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSource +import org.opendc.simulator.flow.source.FixedFlowSource + +/** + * Test suite for the [SimPowerSource] + */ +internal class SimPowerSourceTest { + @Test + fun testInitialState() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + + assertFalse(source.isConnected) + assertNull(source.inlet) + assertEquals(100.0, source.capacity) + } + + @Test + fun testDisconnectIdempotent() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + + assertDoesNotThrow { source.disconnect() } + assertFalse(source.isConnected) + } + + @Test + fun testConnect() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val inlet = SimpleInlet() + + source.connect(inlet) + + assertTrue(source.isConnected) + assertEquals(inlet, source.inlet) + assertTrue(inlet.isConnected) + assertEquals(source, inlet.outlet) + assertEquals(100.0, source.powerDraw) + } + + @Test + fun testDisconnect() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) + val inlet = object : SimPowerInlet() { + override fun createSource(): FlowSource = consumer + } + + source.connect(inlet) + source.disconnect() + + verify { consumer.onStop(any(), any(), any()) } + } + + @Test + fun testDisconnectAssertion() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val inlet = mockk<SimPowerInlet>(relaxUnitFun = true) + every { inlet.isConnected } returns false + every { inlet._outlet } returns null + every { inlet.createSource() } returns FixedFlowSource(100.0, utilization = 1.0) + + source.connect(inlet) + + assertThrows<AssertionError> { + source.disconnect() + } + } + + @Test + fun testOutletAlreadyConnected() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val inlet = SimpleInlet() + + source.connect(inlet) + assertThrows<IllegalStateException> { + source.connect(SimpleInlet()) + } + + assertEquals(inlet, source.inlet) + } + + @Test + fun testInletAlreadyConnected() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val inlet = mockk<SimPowerInlet>(relaxUnitFun = true) + every { inlet.isConnected } returns true + + assertThrows<IllegalStateException> { + source.connect(inlet) + } + } + + class SimpleInlet : SimPowerInlet() { + override fun createSource(): FlowSource = FixedFlowSource(100.0, utilization = 1.0) + } +} diff --git a/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt new file mode 100644 index 00000000..a764a368 --- /dev/null +++ b/opendc-simulator/opendc-simulator-power/src/test/kotlin/org/opendc/simulator/power/SimUpsTest.kt @@ -0,0 +1,101 @@ +/* + * 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.power + +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertAll +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.opendc.simulator.core.runBlockingSimulation +import org.opendc.simulator.flow.FlowEngine +import org.opendc.simulator.flow.FlowSource +import org.opendc.simulator.flow.source.FixedFlowSource + +/** + * Test suite for the [SimUps] class. + */ +internal class SimUpsTest { + @Test + fun testSingleInlet() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + val ups = SimUps(engine) + source.connect(ups.newInlet()) + ups.connect(SimpleInlet()) + + assertEquals(50.0, source.powerDraw) + } + + @Test + fun testDoubleInlet() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source1 = SimPowerSource(engine, capacity = 100.0) + val source2 = SimPowerSource(engine, capacity = 100.0) + val ups = SimUps(engine) + source1.connect(ups.newInlet()) + source2.connect(ups.newInlet()) + + ups.connect(SimpleInlet()) + + assertAll( + { assertEquals(50.0, source1.powerDraw) }, + { assertEquals(50.0, source2.powerDraw) } + ) + } + + @Test + fun testLoss() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source = SimPowerSource(engine, capacity = 100.0) + // https://download.schneider-electric.com/files?p_Doc_Ref=SPD_NRAN-66CK3D_EN + val ups = SimUps(engine, idlePower = 4.0, lossCoefficient = 0.05) + source.connect(ups.newInlet()) + ups.connect(SimpleInlet()) + + assertEquals(56.5, source.powerDraw) + } + + @Test + fun testDisconnect() = runBlockingSimulation { + val engine = FlowEngine(coroutineContext, clock) + val source1 = SimPowerSource(engine, capacity = 100.0) + val source2 = SimPowerSource(engine, capacity = 100.0) + val ups = SimUps(engine) + source1.connect(ups.newInlet()) + source2.connect(ups.newInlet()) + val consumer = spyk(FixedFlowSource(100.0, utilization = 1.0)) + val inlet = object : SimPowerInlet() { + override fun createSource(): FlowSource = consumer + } + + ups.connect(inlet) + ups.disconnect() + + verify { consumer.onStop(any(), any(), any()) } + } + + class SimpleInlet : SimPowerInlet() { + override fun createSource(): FlowSource = FixedFlowSource(100.0, utilization = 0.5) + } +} |
