From 3396ef7e49d5965c9201737c4c1c40385afe24c3 Mon Sep 17 00:00:00 2001 From: Neloreck Date: Wed, 1 Nov 2023 02:52:58 +0200 Subject: [PATCH] Tests for run action state. --- TODO.md | 1 + src/AbstractAction.ts | 2 +- src/state_machine/FiniteStateMachine.ts | 8 +- src/state_machine/RunActionState.test.ts | 131 +++++++++++++++++++++++ src/state_machine/RunActionState.ts | 7 +- src/utils/graph/DirectedGraph.test.ts | 11 ++ test/fixtures/mocks/GenericAction.ts | 28 ++--- 7 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 src/state_machine/RunActionState.test.ts diff --git a/TODO.md b/TODO.md index 1e83e8b..fa58bca 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/src/AbstractAction.ts b/src/AbstractAction.ts index 2ad8d83..ff33375 100644 --- a/src/AbstractAction.ts +++ b/src/AbstractAction.ts @@ -30,7 +30,7 @@ export abstract class AbstractAction { * @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. diff --git a/src/state_machine/FiniteStateMachine.ts b/src/state_machine/FiniteStateMachine.ts index ed6459b..ebc78c7 100644 --- a/src/state_machine/FiniteStateMachine.ts +++ b/src/state_machine/FiniteStateMachine.ts @@ -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 = this.states.pop(); if (state instanceof RunActionState) { diff --git a/src/state_machine/RunActionState.test.ts b/src/state_machine/RunActionState.test.ts new file mode 100644 index 0000000..b715f05 --- /dev/null +++ b/src/state_machine/RunActionState.test.ts @@ -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 = [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 = [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 = []; + 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 = [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 = [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 = [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)]); + }); +}); diff --git a/src/state_machine/RunActionState.ts b/src/state_machine/RunActionState.ts index b991b26..f0f6a05 100644 --- a/src/state_machine/RunActionState.ts +++ b/src/state_machine/RunActionState.ts @@ -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; @@ -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)) { diff --git a/src/utils/graph/DirectedGraph.test.ts b/src/utils/graph/DirectedGraph.test.ts index 3ea9671..941c485 100644 --- a/src/utils/graph/DirectedGraph.test.ts +++ b/src/utils/graph/DirectedGraph.test.ts @@ -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 = 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 = createBasicTestGraph(5); diff --git a/test/fixtures/mocks/GenericAction.ts b/test/fixtures/mocks/GenericAction.ts index 910afc0..8e2c9bd 100644 --- a/test/fixtures/mocks/GenericAction.ts +++ b/test/fixtures/mocks/GenericAction.ts @@ -1,3 +1,5 @@ +import { jest } from "@jest/globals"; + import { AbstractAction } from "@/AbstractAction"; import { IUnit } from "@/unit/IUnit"; @@ -6,29 +8,29 @@ export class GenericAction extends AbstractAction { 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(); }