Simpler simulation tests

<
>
January 13, 2022

While I already added quite a few helper functions for the definition of simulation tests, it still was a lot of work and pretty annoying to add one. As an example:

#[test]
fn source_to_sink_splitter_hole() {
    let duration = 5000;
    type S = StructureAction;
    let actions_struct = vec![
        S::WantAdd(
            PosAnchor(Pos::default()),
            (&Source::new(Material::Coal.into())).into(),
        ),
        S::WantAdd(PosAnchor(Pos { y: 0, x: 1 }), (&Arm::e()).into()),
        S::WantAdd(PosAnchor(Pos { y: 0, x: 2 }), (&Splitter::e()).into()),
        S::WantAdd(PosAnchor(Pos { y: 0, x: 3 }), (&Arm::e()).into()),
        S::WantAdd(PosAnchor(Pos { y: 1, x: 3 }), (&Arm::e()).into()),
        S::WantAdd(PosAnchor(Pos { y: 0, x: 4 }), (&Sink::default()).into()),
        S::WantAdd(PosAnchor(Pos { y: 1, x: 4 }), (&Hole::default()).into()),
    ];
    let module = Ok(ModuleCheckData::new(
        [5, 2].into(),
        vec![ModuleSource {
            pos: [0, 0],
            item: Material::Coal.into(),
            count: 26,
        }],
        vec![ModuleSink {
            pos: [4, 0],
            item: Material::Coal.into(),
            count: 13,
        }],
        vec![
            (3, s_arm()),
            (1, s_source()),
            (1, s_sink()),
            (1, s_splitter()),
            (1, s_hole()),
        ],
        duration,
    ));
    check(actions_struct, duration, module);
}

This test moves Items from a Source via an Arm onto a Splitter from which two Arms will grab the Items and put them either into a Hole (a special Structure for tests that simply deletes Items passed to it) or a Sink.
The simulation is then run for 5000 ticks, and is expected to create a Module of size 5x2, take 26 Coal from the Source and place 13 of them into the Sink. The other half is deleted via the Hole.

As you can see writing such a test is quite involved.

Test parser

To reduce the overhead for writing a test I wrote a custom parser for test files. It parses text into one of the following commands:

pub enum Cmd {
    Add(AddAction),
    SetDuration(u32),
    SetExpectedDuration(u32),
    SetExpectedSize(Size<u32>),
    SetError(ModularizeError),
    AddExpectedSource(PosAnchor<i64>, usize, Item),
    AddExpectedSink(PosAnchor<i64>, usize, Item),
}

pub struct AddAction {
    pub pos: PosAnchor<i64>,
    pub rot: Rot,
    pub structure: StructureTemplate,
}

Once the commands are parsed, a SimulationTest can be generated and run from them:

pub struct SimulationTest {
    error: Option<ModularizeError>,
    actions: Vec<AddAction>,
    duration: u32,
    e_duration: u32,
    e_size: Size<u32>,
    e_sources: Vec<(PosAnchor<i64>, usize, Item)>,
    e_sinks: Vec<(PosAnchor<i64>, usize, Item)>,
}

impl SimulationTest {
    pub fn run(&self) {
        ...
    }

    pub fn update(&mut self, cmd: Cmd) {
        match cmd {
            Cmd::Add(x) => self.actions.push(x),
            ...
        }
    }
    ...
}

With all those helpers the test from above can now be written as:

duration 5000
e_duration 5000
e_size 5/2
+ 0/0 0 source coal
+ 1/0 0 arm
+ 2/0 0 splitter
+ 3/0 0 arm
+ 3/1 0 arm
+ 4/0 0 sink
+ 4/1 0 hole
e_source 0/0 26 coal
e_sink 4/0 13 coal