Back to Repositories

Validating Power System Dynamics in Mindustry

This test suite validates the power system functionality in Mindustry, focusing on PowerGraph behavior, consumer-producer relationships, and battery mechanics. It implements comprehensive testing of power flow, consumption patterns, and system state management.

Test Coverage Overview

The test suite provides extensive coverage of power system dynamics in Mindustry.

Key areas tested include:
  • Direct power consumption and production scenarios
  • Battery capacity and charge management
  • Power graph updates and state transitions
  • Consumer satisfaction levels under various conditions
  • Edge cases including floating-point precision handling

Implementation Analysis

The testing approach utilizes JUnit 5’s nested test classes and dynamic test factories for parameterized testing scenarios.

Notable patterns include:
  • Fixed delta time testing (0.5) for deterministic results
  • Isolated thread frame execution for power graph updates
  • Simulation of producer-consumer relationships
  • Precise floating-point comparison with error margins

Technical Details

Testing infrastructure includes:
  • JUnit Jupiter test framework
  • Custom PowerTestFixture base class
  • FakeThreadHandler for thread simulation
  • Mock implementations of power-related blocks
  • Utility classes for tile and block creation

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices:

  • Comprehensive documentation of test assumptions and constraints
  • Systematic test organization using nested classes
  • Parameterized testing for multiple scenarios
  • Clear separation of test setup and verification
  • Precise assertion messages for debugging

anuken/mindustry

tests/src/test/java/power/PowerTests.java

            
package power;

import arc.math.*;
import arc.util.*;
import mindustry.*;
import mindustry.core.*;
import mindustry.gen.*;
import mindustry.world.*;
import mindustry.world.blocks.power.PowerGenerator.*;
import mindustry.world.blocks.power.*;
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicTest.*;

/**
 * Tests code related to the power system in general, but not specific blocks.
 * All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well.
 * Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame.
 * Both of these constraints are handled by FakeThreadHandler within PowerTestFixture.
 * Any power amount (produced, consumed, buffered) should be affected by Time.delta() but status should not!
 */
public class PowerTests extends PowerTestFixture{

    @BeforeAll
    static void init(){
        Vars.state = new GameState();
    }

    @Nested
    class PowerGraphTests{

        /**
         * Tests the status of a single consumer after a single update of the power graph which contains a single producer.
         * <p>
         * Assumption: When the consumer requests zero power, status does not change. Default is 0.0f.
         */
        @TestFactory
        DynamicTest[] directConsumerSatisfactionIsAsExpected(){
            return new DynamicTest[]{
            // Note: Unfortunately, the display names are not yet output through gradle. See https://github.com/gradle/gradle/issues/5975
            // That's why we inject the description into the test method for now.
            // Additional Note: If you don't see any labels in front of the values supplied as function parameters, use a better IDE like IntelliJ IDEA.
            dynamicTest("01", () -> simulateDirectConsumption(0.0f, 1.0f, 0.0f, "0.0 produced, 1.0 consumed (no power available)")),
            dynamicTest("02", () -> simulateDirectConsumption(0.0f, 0.0f, 0.0f, "0.0 produced, 0.0 consumed (no power anywhere)")),
            dynamicTest("03", () -> simulateDirectConsumption(1.0f, 0.0f, 1.0f, "1.0 produced, 0.0 consumed (no power requested)")),
            dynamicTest("04", () -> simulateDirectConsumption(1.0f, 1.0f, 1.0f, "1.0 produced, 1.0 consumed (stable consumption)")),
            dynamicTest("05", () -> simulateDirectConsumption(0.5f, 1.0f, 0.5f, "0.5 produced, 1.0 consumed (power shortage)")),
            dynamicTest("06", () -> simulateDirectConsumption(1.0f, 0.5f, 1.0f, "1.0 produced, 0.5 consumed (power excess)")),
            dynamicTest("07", () -> simulateDirectConsumption(0.09f, 0.09f - Mathf.FLOAT_ROUNDING_ERROR / 10.0f, 1.0f, "floating point inaccuracy (stable consumption)"))
            };
        }

        void simulateDirectConsumption(float producedPower, float requiredPower, float expectedSatisfaction, String parameterDescription){
            Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower));
            ((GeneratorBuild)producerTile.build).productionEfficiency = 1f;
            Tile directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requiredPower));

            PowerGraph powerGraph = new PowerGraph();
            powerGraph.add(producerTile.build);
            powerGraph.add(directConsumerTile.build);

            for(Building build : powerGraph.all) build.updateConsumption();

            assertEquals(producedPower * Time.delta, powerGraph.getPowerProduced(), Mathf.FLOAT_ROUNDING_ERROR);
            assertEquals(requiredPower * Time.delta, powerGraph.getPowerNeeded(), Mathf.FLOAT_ROUNDING_ERROR);

            //Update and check for the expected power status of the consumer
            powerGraph.update();
            assertEquals(expectedSatisfaction, directConsumerTile.build.power.status, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match");
        }

        /**
         * Tests the status of a single direct consumer after a single update of the power graph which contains a single producer and a single battery.
         * The used battery is created with a maximum capacity of 100 and receives ten power per tick.
         */
        @TestFactory
        DynamicTest[] batteryCapacityIsAsExpected(){
            return new DynamicTest[]{
            // Note: expectedBatteryCapacity is currently adjusted to a delta of 0.5! (FakeThreadHandler sets it to that)
            dynamicTest("01", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 0.0f, 5.0f, 0.0f, "Empty battery, no consumer")),
            dynamicTest("02", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 94.999f, 99.999f, 0.0f, "Battery almost full after update, no consumer")),
            dynamicTest("03", () -> simulateDirectConsumptionWithBattery(10.0f, 0.0f, 100.0f, 100.0f, 0.0f, "Full battery, no consumer")),
            dynamicTest("04", () -> simulateDirectConsumptionWithBattery(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, "No producer, no consumer, empty battery")),
            dynamicTest("05", () -> simulateDirectConsumptionWithBattery(0.0f, 0.0f, 100.0f, 100.0f, 0.0f, "No producer, no consumer, full battery")),
            dynamicTest("06", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 0.0f, 0.0f, 0.0f, "No producer, empty battery")),
            dynamicTest("07", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 100.0f, 95.0f, 1.0f, "No producer, full battery")),
            dynamicTest("08", () -> simulateDirectConsumptionWithBattery(0.0f, 10.0f, 2.5f, 0.0f, 0.5f, "No producer, low battery")),
            dynamicTest("09", () -> simulateDirectConsumptionWithBattery(5.0f, 10.0f, 5.0f, 2.5f, 1.0f, "Producer + Battery = Consumed")),
            };
        }

        void simulateDirectConsumptionWithBattery(float producedPower, float requestedPower, float initialBatteryCapacity, float expectedBatteryCapacity, float expectedSatisfaction, String parameterDescription){
            PowerGraph powerGraph = new PowerGraph();

            if(producedPower > 0.0f){
                Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower));
                ((GeneratorBuild)producerTile.build).productionEfficiency = 1f;
                powerGraph.add(producerTile.build);
            }
            Tile directConsumerTile = null;
            if(requestedPower > 0.0f){
                directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requestedPower));
                powerGraph.add(directConsumerTile.build);
            }
            float maxCapacity = 100f;
            Tile batteryTile = createFakeTile(0, 2, createFakeBattery(maxCapacity));
            batteryTile.build.power.status = initialBatteryCapacity / maxCapacity;

            powerGraph.add(batteryTile.build);

            for(Building build : powerGraph.all) build.updateConsumption();

            powerGraph.update();

            assertEquals(expectedBatteryCapacity / maxCapacity, batteryTile.build.power.status, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Expected battery status did not match");
            if(directConsumerTile != null){
                assertEquals(expectedSatisfaction, directConsumerTile.build.power.status, Mathf.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match");
            }
        }

        /** Makes sure a direct consumer stops working after power production is set to zero. */
        @Test
        void directConsumptionStopsWithNoPower(){
            Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(10.0f));
            ((GeneratorBuild)producerTile.build).productionEfficiency = 1.0f;
            Tile consumerTile = createFakeTile(0, 1, createFakeDirectConsumer(5.0f));

            PowerGraph powerGraph = new PowerGraph();
            powerGraph.add(producerTile.build);
            powerGraph.add(consumerTile.build);
            powerGraph.update();

            assertEquals(1.0f, consumerTile.build.power.status, Mathf.FLOAT_ROUNDING_ERROR);

            powerGraph.removeList(producerTile.build);
            powerGraph.add(consumerTile.build);
            powerGraph.update();

            assertEquals(0.0f, consumerTile.build.power.status, Mathf.FLOAT_ROUNDING_ERROR);
            if(consumerTile.block().consPower != null){
                assertEquals(0f, consumerTile.block().consPower.efficiency(consumerTile.build));
            }
        }
    }
}