Skip to content

Commit

Permalink
Tests for run action state.
Browse files Browse the repository at this point in the history
  • Loading branch information
Neloreck committed Nov 1, 2023
1 parent 34173ca commit 3396ef7
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 21 deletions.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## List of todo tasks

- Add event emitter abstraction instead of interfaces + Java style
- Swap state machine execute condition - true finished / false not finished
2 changes: 1 addition & 1 deletion src/AbstractAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export abstract class AbstractAction<T = any> {
* @param unit - the unit the action is checked for
* @returns success of the action, returning true causes the swap to the next action in the queue
*/
public abstract isDone(unit: IUnit): boolean;
public abstract isFinished(unit: IUnit): boolean;

/**
* Gets called when the action is going to be executed by the Unit.
Expand Down
8 changes: 4 additions & 4 deletions src/state_machine/FiniteStateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ export class FiniteStateMachine {
*/
public update(unit: IUnit): void {
try {
if (this.states.length && !stackPeek(this.states).execute(unit)) {
if (this.states.pop() instanceof RunActionState) {
this.dispatchNewPlanFinishedEvent();
}
// When stack action execution is finished, pop latest action and notify listeners if needed.
if (this.states.length && !stackPeek(this.states).execute(unit) && this.states.pop() instanceof RunActionState) {
this.dispatchNewPlanFinishedEvent();
}
} catch (error) {
// Pop problematic action and notify listeners if needed.
const state: Maybe<IFiniteStateMachineState> = this.states.pop();

if (state instanceof RunActionState) {
Expand Down
131 changes: 131 additions & 0 deletions src/state_machine/RunActionState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { describe, expect, it, jest } from "@jest/globals";

import { GenericAction } from "#/fixtures/mocks";

import { AbstractAction } from "@/AbstractAction";
import { FiniteStateMachine } from "@/state_machine/FiniteStateMachine";
import { MoveToState } from "@/state_machine/MoveToState";
import { RunActionState } from "@/state_machine/RunActionState";
import { Queue } from "@/types";
import { IUnit } from "@/unit/IUnit";

describe("RunActionState class", () => {
it("should correctly initialize", () => {
const fsm: FiniteStateMachine = new FiniteStateMachine();
const plan: Queue<AbstractAction> = [new GenericAction(1), new GenericAction(2)];
const state: RunActionState = new RunActionState(fsm, plan);

expect(state.getCurrentPlan()).toBe(plan);
});

it("should correctly handle throw NotPerformableActionException if action condition fails", () => {
const first: AbstractAction = new GenericAction(1);
const second: AbstractAction = new GenericAction(1);
const fsm: FiniteStateMachine = new FiniteStateMachine();
const plan: Queue<AbstractAction> = [first, second];
const state: RunActionState = new RunActionState(fsm, plan);

jest.spyOn(first, "performAction").mockImplementation(() => false);
expect(state.execute({} as IUnit)).toBe(false);
});

it("should correctly handle empty plan", () => {
const fsm: FiniteStateMachine = new FiniteStateMachine();
const plan: Queue<AbstractAction> = [];
const state: RunActionState = new RunActionState(fsm, plan);

expect(state.execute({} as IUnit)).toBe(false);
});

it("should correctly remove all actions that are done and reset them", () => {
const first: AbstractAction = new GenericAction(1);
const second: AbstractAction = new GenericAction(1);
const fsm: FiniteStateMachine = new FiniteStateMachine();
const plan: Queue<AbstractAction> = [first, second];
const state: RunActionState = new RunActionState(fsm, plan);

jest.spyOn(first, "isFinished").mockImplementation(() => true);
jest.spyOn(first, "reset").mockImplementation(() => {});
jest.spyOn(second, "isFinished").mockImplementation(() => true);
jest.spyOn(second, "reset").mockImplementation(() => {});

expect(state.execute({} as IUnit)).toBe(false);
expect(state.getCurrentPlan()).toEqual([]);

expect(first.isFinished).toHaveBeenCalledTimes(1);
expect(first.reset).toHaveBeenCalledTimes(1);
expect(second.isFinished).toHaveBeenCalledTimes(1);
expect(second.reset).toHaveBeenCalledTimes(1);
});

it("should correctly handle not completed actions and execute them", () => {
const first: AbstractAction = new GenericAction(1);
const second: AbstractAction = new GenericAction(1);
const third: AbstractAction = new GenericAction(1);
const fsm: FiniteStateMachine = new FiniteStateMachine();
const plan: Queue<AbstractAction> = [first, second, third];
const state: RunActionState = new RunActionState(fsm, plan);
const unit: IUnit = {} as IUnit;

jest.spyOn(first, "isFinished").mockImplementation(() => true);
jest.spyOn(first, "reset").mockImplementation(() => {});

jest.spyOn(second, "requiresInRange").mockImplementation(() => true);
jest.spyOn(second, "isInRange").mockImplementation(() => true);

expect(state.execute(unit)).toBe(true);
expect(state.getCurrentPlan()).toEqual([second, third]);

expect(first.isFinished).toHaveBeenCalledTimes(1);
expect(first.reset).toHaveBeenCalledTimes(1);
expect(second.checkProceduralPrecondition).toHaveBeenCalledTimes(1);
expect(second.checkProceduralPrecondition).toHaveBeenCalledWith(unit);
expect(second.performAction).toHaveBeenCalledTimes(1);
expect(second.performAction).toHaveBeenCalledWith(unit);
expect(third.checkProceduralPrecondition).not.toHaveBeenCalled();
expect(third.performAction).not.toHaveBeenCalled();

expect(state.execute(unit)).toBe(true);
expect(state.getCurrentPlan()).toEqual([second, third]);
expect(second.performAction).toHaveBeenCalledTimes(2);
expect(third.performAction).not.toHaveBeenCalled();

jest.spyOn(second, "checkProceduralPrecondition").mockImplementation(() => false);

expect(state.execute(unit)).toBe(true);
expect(state.getCurrentPlan()).toEqual([second, third]);
expect(second.performAction).toHaveBeenCalledTimes(2);
expect(third.performAction).not.toHaveBeenCalled();
});

it("should correctly add react target state to FSM if needed", () => {
const first: AbstractAction = new GenericAction(1);
const second: AbstractAction = new GenericAction(1);
const third: AbstractAction = new GenericAction(1);
const fsm: FiniteStateMachine = new FiniteStateMachine();
const plan: Queue<AbstractAction> = [first, second, third];
const state: RunActionState = new RunActionState(fsm, plan);
const unit: IUnit = {} as IUnit;

fsm.push(state);

jest.spyOn(first, "isFinished").mockImplementation(() => true);
jest.spyOn(first, "reset").mockImplementation(() => {});

jest.spyOn(second, "requiresInRange").mockImplementation(() => true);
jest.spyOn(second, "isInRange").mockImplementation(() => false);

expect(state.execute(unit)).toBe(true);
expect(state.getCurrentPlan()).toEqual([second, third]);

expect(first.isFinished).toHaveBeenCalledTimes(1);
expect(first.reset).toHaveBeenCalledTimes(1);
expect(second.checkProceduralPrecondition).not.toHaveBeenCalled();
expect(second.performAction).not.toHaveBeenCalled();
expect(third.checkProceduralPrecondition).not.toHaveBeenCalled();
expect(third.performAction).not.toHaveBeenCalled();

// Verify that move to target state is pushed.
expect(fsm.getStack()).toEqual([state, new MoveToState(second)]);
});
});
7 changes: 4 additions & 3 deletions src/state_machine/RunActionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ export class RunActionState implements IFiniteStateMachineState {
}

/**
* Cycle through all actions until an invalid one or the end of the Queue is reached.
* Cycle through all actions until an invalid one or the end of the plan is reached.
* A false return type here causes the FSM to pop the state from its stack.
*/
public execute(unit: IUnit): boolean {
try {
// Find first action that is not done.
// Shift all completed actions from the queue and reset them.
for (;;) {
if (this.plan.length && queuePeek(this.plan).isDone(unit)) {
while (this.plan.length) {
if (queuePeek(this.plan).isFinished(unit)) {
this.plan.shift().reset();
} else {
break;
Expand All @@ -54,6 +54,7 @@ export class RunActionState implements IFiniteStateMachineState {
// System.out.println("Target is null! " + currentAction.getClass().getSimpleName());
}

// Should handle some movement conditions before continuation of execution.
if (currentAction.requiresInRange(unit) && !currentAction.isInRange(unit)) {
this.fsm.push(new MoveToState(currentAction));
} else if (currentAction.checkProceduralPrecondition(unit)) {
Expand Down
11 changes: 11 additions & 0 deletions src/utils/graph/DirectedGraph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import { DirectedGraph } from "@/utils/graph/DirectedGraph";
import { Edge } from "@/utils/graph/Edge";

describe("DirectedGraph class", () => {
it("should correctly initialize from set", () => {
const graph: DirectedGraph<number, Edge> = new DirectedGraph(new Set([1, 3, 5]));

expect(graph.getEdges().size).toBe(0);
expect(graph.getVertices().size).toBe(3);

expect(graph.getVertices().has(1)).toBe(true);
expect(graph.getVertices().has(3)).toBe(true);
expect(graph.getVertices().has(5)).toBe(true);
});

it("should correctly initialize basic graph", () => {
const graph: DirectedGraph<number, Edge> = createBasicTestGraph(5);

Expand Down
28 changes: 15 additions & 13 deletions test/fixtures/mocks/GenericAction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { jest } from "@jest/globals";

import { AbstractAction } from "@/AbstractAction";
import { IUnit } from "@/unit/IUnit";

Expand All @@ -6,29 +8,29 @@ export class GenericAction<T = unknown> extends AbstractAction<T> {
return 0;
}

public generateCostRelativeToTarget(unit: IUnit): number {
public generateCostRelativeToTarget = jest.fn((unit: IUnit): number => {
return 0;
}
});

public checkProceduralPrecondition(unit: IUnit): boolean {
public checkProceduralPrecondition = jest.fn((unit: IUnit): boolean => {
return true;
}
});

public performAction(unit: IUnit): boolean {
public performAction = jest.fn((unit: IUnit): boolean => {
return true;
}
});

public isDone(unit: IUnit): boolean {
public isFinished = jest.fn((unit: IUnit): boolean => {
return false;
}
});

public isInRange(unit: IUnit): boolean {
public isInRange = jest.fn((unit: IUnit): boolean => {
return false;
}
});

public requiresInRange(unit: IUnit): boolean {
public requiresInRange = jest.fn((unit: IUnit): boolean => {
return false;
}
});

public reset(): void {}
public reset = jest.fn();
}

0 comments on commit 3396ef7

Please sign in to comment.