From 2b1936c82dcceb829a78c56d375925e47cd6d0bf Mon Sep 17 00:00:00 2001 From: BusyCityGuy <55513323+BusyCityGuy@users.noreply.github.com> Date: Mon, 13 May 2024 01:10:14 -0700 Subject: [PATCH 1/7] Dump in-progress WIP tests --- .../Source/Tests/StateMachine.spec.lua | 1070 ++++++++++++++++- 1 file changed, 1066 insertions(+), 4 deletions(-) diff --git a/src/TestService/Source/Tests/StateMachine.spec.lua b/src/TestService/Source/Tests/StateMachine.spec.lua index f6be6cb..011507e 100644 --- a/src/TestService/Source/Tests/StateMachine.spec.lua +++ b/src/TestService/Source/Tests/StateMachine.spec.lua @@ -1,4 +1,5 @@ ---!strict +--!nonstrict +-- FIXME: Change to strict and fix type issues local ReplicatedStorage = game:GetService("ReplicatedStorage") local TestService = game:GetService("TestService") @@ -6,6 +7,7 @@ local TestService = game:GetService("TestService") local JestGlobals = require(TestService.Dependencies.JestGlobals) local Freeze = require(TestService.Dependencies.Freeze) local Logger = require(ReplicatedStorage.Source.StateMachine.Modules.Logger) +local Signal = require(ReplicatedStorage.Source.StateMachine.Modules.Signal) local StateMachine = require(ReplicatedStorage.Source.StateMachine) -- Shortening things is generally bad practice, but this greatly improves readability of tests @@ -50,8 +52,1068 @@ local TO_X_HANDLER = to(X_STATE) local TO_Y_HANDLER = to(Y_STATE) local FINISH_HANDLER = to(FINISH_STATE) -describe("test", function() - it("should pass", function() - expect(true).toBe(true) +local function plural(count: number) + return if count == 1 then "" else "s" +end + +-- TODO: Evaluate each test to see if it could be done better in Jest. These are currently just translated directly from TestEZ. +-- Done +describe("new", function() + describe("should error iff given bad parameter type for", function() + it("initial state", function() + local eventsByName = { + [TO_X_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local badTypes: { any } = { + 1, + true, + nil, + {}, + } + + local goodType = X_STATE + + for _, badType in badTypes do + expect(function() + StateMachine.new(badType, eventsByName) + end).toThrow("Bad tuple index #1") + end + + expect(function() + StateMachine.new(goodType, eventsByName) + end).never.toThrow() + end) + + it("events", function() + local goodEventsByName = { + [TO_X_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local badTypes: { any } = { + 1, + true, + nil, + { + "bad", + "type", + }, + } + + for _, badType in badTypes do + expect(function() + StateMachine.new(X_STATE, badType) + end).toThrow("Bad tuple index #2") + end + + expect(function() + StateMachine.new(X_STATE, goodEventsByName) + end).never.toThrow() + end) + + it("name", function() + local eventsByName = { + [TO_X_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local badTypes: { any } = { + 1, + true, + nil, + {}, + } + + local goodTypes: { any } = { + nil, + "good type", + "", + } + + for _, badType in badTypes do + expect(function() + StateMachine.new(X_STATE, eventsByName, badType) + end).toThrow("Bad tuple index #3") + end + + for _, goodType in goodTypes do + expect(function() + StateMachine.new(X_STATE, eventsByName, goodType) + end).never.toThrow() + end + end) + + it("log level", function() + local eventsByName = { + [TO_X_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local badTypes: { any } = { + 1, + true, + nil, + {}, + "bad type", + } + + local goodTypes: { nil | Logger.LogLevel } = { + nil, + StateMachine.Logger.LogLevel.Error, + StateMachine.Logger.LogLevel.Warn, + StateMachine.Logger.LogLevel.Info, + StateMachine.Logger.LogLevel.Debug, + } + + for _, badType in badTypes do + expect(function() + StateMachine.new(X_STATE, eventsByName, nil, badType) + end).toThrow("Bad tuple index #4") + end + + for _, goodType: nil | Logger.LogLevel in goodTypes do + expect(function() + StateMachine.new(X_STATE, eventsByName, nil, goodType) + end).never.toThrow() + end + end) + end) + + it("should return a new state machine", function() + local initialState = X_STATE + local eventsByName = { + [TO_X_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local stateMachine1 = StateMachine.new(initialState, eventsByName) + local stateMachine2 = StateMachine.new(initialState, eventsByName) + + expect(stateMachine1).toBeInstanceOf(StateMachine) + expect(stateMachine2).toBeInstanceOf(StateMachine) + expect(stateMachine1 == stateMachine2).toBe(false) + end) + + it("should set initial state", function() + local initialState = X_STATE + local eventsByName = { + [TO_X_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local stateMachine = StateMachine.new(initialState, eventsByName) + expect(stateMachine._currentState).toBe(initialState) + end) + + it("should set valid event names by state", function() + local eventsByName = { + [TO_Y_EVENT] = { + canBeFinal = false, + from = { + [X_STATE] = { + beforeAsync = TO_Y_HANDLER, + }, + [Y_STATE] = { + beforeAsync = TO_Y_HANDLER, + }, + }, + }, + [TO_X_EVENT] = { + canBeFinal = false, + from = { + [Y_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local stateMachine = StateMachine.new(X_STATE, eventsByName) + local validEventNamesFromX = stateMachine._validEventNamesByState[X_STATE] + local validEventNamesFromY = stateMachine._validEventNamesByState[Y_STATE] + + expect(validEventNamesFromX).toEqual(expect.any("table")) + expect(validEventNamesFromY).toEqual(expect.any("table")) + + expect(Dict.count(validEventNamesFromX)).toBe(1) + expect(Dict.includes(validEventNamesFromX, TO_Y_EVENT)).toBe(true) + + expect(Dict.count(validEventNamesFromY)).toBe(2) + expect(Dict.includes(validEventNamesFromY, TO_X_EVENT)).toBe(true) + expect(Dict.includes(validEventNamesFromY, TO_Y_EVENT)).toBe(true) + end) + + it("should set handlers by event name", function() + local eventsByName = { + [TO_Y_EVENT] = { + canBeFinal = false, + from = { + [X_STATE] = { + beforeAsync = TO_Y_HANDLER, + }, + [Y_STATE] = { + beforeAsync = TO_Y_HANDLER, + }, + }, + }, + [TO_X_EVENT] = { + canBeFinal = false, + from = { + [Y_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local stateMachine = StateMachine.new(X_STATE, eventsByName) + local handlers = stateMachine._handlersByEventName + + expect(Dict.count(handlers)).toBe(2) + expect(handlers[TO_X_EVENT]).toEqual(expect.any("function")) + expect(handlers[TO_Y_EVENT]).toEqual(expect.any("function")) + end) + + it("should make signals available", function() + local eventsByName = { + [TO_X_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local stateMachine = StateMachine.new(X_STATE, eventsByName) + + expect(stateMachine[BEFORE_EVENT_SIGNAL]).never.toBeNil() + expect(stateMachine[LEAVING_STATE_SIGNAL]).never.toBeNil() + expect(stateMachine[STATE_ENTERED_SIGNAL]).never.toBeNil() + expect(stateMachine[AFTER_EVENT_SIGNAL]).never.toBeNil() + expect(stateMachine[FINISHED_SIGNAL]).never.toBeNil() + end) +end) + +-- Done +describe("handle", function() + it("should error given a bad event name", function() + local nonexistentEventName = "nonexistent" + local eventsByName = { + [TO_X_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + } + + local stateMachine = StateMachine.new(X_STATE, eventsByName) + + local badTypes: { any } = { + 1, + true, + nil, + {}, + } + + for _, badType in badTypes do + expect(function() + stateMachine:handle(badType) + end).toThrow(`string expected, got {typeof(badType)}`) + end + + expect(function() + stateMachine:handle(nonexistentEventName) + end).toThrow(`Invalid event name passed to handle: {nonexistentEventName}`) + end) + + describe("should fire signals", function() + it("in the correct order", function(_, done) + local initialState = X_STATE + local eventsByName = { + [FINISH_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = FINISH_HANDLER, + }, + }, + }, + } + + local stateMachine = StateMachine.new(initialState, eventsByName) + local resultSignalOrder: { string } = {} + local signalConnections: { Signal.SignalConnection } = {} + + -- Set up event connections + for _, signalName in ORDERED_SIGNALS do + local newSignalConnection = (stateMachine[signalName] :: Signal.ClassType):Connect(function(_, _) + table.insert(resultSignalOrder, signalName) + + if #resultSignalOrder == #ORDERED_SIGNALS then + for _, signalConnection in signalConnections do + signalConnection:Disconnect() + end + xpcall(function() + expect(resultSignalOrder).toEqual(ORDERED_SIGNALS) + done() + end, function(err) + done(err) + end) + end + end) + + table.insert(signalConnections, newSignalConnection) + end + + stateMachine:handle(FINISH_EVENT) + end, 500) + + describe(`with the correct parameters and state`, function() + local initialState = X_STATE + local variadicArgs = { "test", false, nil, 3.5 } + local timeout = 0.5 + local handledEventName = TO_Y_EVENT + local receivedParameters + local eventsByName = { + [TO_Y_EVENT] = { + canBeFinal = false, + from = { + [X_STATE] = { + beforeAsync = TO_Y_HANDLER, + }, + }, + }, + [TO_X_EVENT] = { + canBeFinal = false, + from = { + [Y_STATE] = { + beforeAsync = TO_X_HANDLER, + }, + }, + }, + [FINISH_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = FINISH_HANDLER, + }, + }, + }, + } + + local stateMachine + + beforeEach(function() + receivedParameters = nil + stateMachine = StateMachine.new(initialState, eventsByName) + end) + + it(BEFORE_EVENT_SIGNAL, function() + local mainThread = coroutine.running() + local timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, true) + end) + + local signalConnection = stateMachine[BEFORE_EVENT_SIGNAL]:Connect(function(...) + receivedParameters = { ... } + task.cancel(timeoutThread) + coroutine.resume(mainThread, false) + end) + + stateMachine:handle(handledEventName, table.unpack(variadicArgs)) + + local didTimeOut = coroutine.yield(mainThread) + signalConnection:Disconnect() + expect(didTimeOut).toBe(false) + expect(#receivedParameters).toBe(2) + expect(receivedParameters[1]).toBe(handledEventName) + expect(receivedParameters[2]).toBe(initialState) + expect(stateMachine._currentState).toBe(initialState) + end) + + it(LEAVING_STATE_SIGNAL, function() + local expectedAfterState = Y_STATE + local mainThread = coroutine.running() + local timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, true) + end) + + local signalConnection = stateMachine[LEAVING_STATE_SIGNAL]:Connect(function(...) + receivedParameters = { ... } + task.cancel(timeoutThread) + coroutine.resume(mainThread, false) + end) + + stateMachine:handle(handledEventName, table.unpack(variadicArgs)) + + local didTimeOut = coroutine.yield(mainThread) + signalConnection:Disconnect() + expect(didTimeOut).toBe(false) + expect(#receivedParameters).toBe(2) + expect(receivedParameters[1]).toBe(initialState) + expect(receivedParameters[2]).toBe(expectedAfterState) + expect(stateMachine._currentState).toBe(initialState) + end) + + it(STATE_ENTERED_SIGNAL, function() + local expectedAfterState = Y_STATE + local mainThread = coroutine.running() + local timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, true) + end) + + local signalConnection = stateMachine[STATE_ENTERED_SIGNAL]:Connect(function(...) + receivedParameters = { ... } + task.cancel(timeoutThread) + coroutine.resume(mainThread, false) + end) + + stateMachine:handle(handledEventName, table.unpack(variadicArgs)) + + local didTimeOut = coroutine.yield(mainThread) + signalConnection:Disconnect() + expect(didTimeOut).toBe(false) + expect(#receivedParameters).toBe(2) + expect(receivedParameters[1]).toBe(expectedAfterState) + expect(receivedParameters[2]).toBe(initialState) + expect(stateMachine._currentState).toBe(expectedAfterState) + end) + + it(AFTER_EVENT_SIGNAL, function() + local expectedAfterState = Y_STATE + local mainThread = coroutine.running() + local timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, true) + end) + + local signalConnection = stateMachine[AFTER_EVENT_SIGNAL]:Connect(function(...) + receivedParameters = { ... } + task.cancel(timeoutThread) + coroutine.resume(mainThread, false) + end) + + stateMachine:handle(handledEventName, table.unpack(variadicArgs)) + + local didTimeOut = coroutine.yield(mainThread) + signalConnection:Disconnect() + expect(didTimeOut).toBe(false) + expect(#receivedParameters).toBe(3) + expect(receivedParameters[1]).toBe(handledEventName) + expect(receivedParameters[2]).toBe(expectedAfterState) + expect(receivedParameters[3]).toBe(initialState) + expect(stateMachine._currentState).toBe(expectedAfterState) + end) + + it(FINISHED_SIGNAL, function() + local mainThread = coroutine.running() + local timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, true) + end) + + local signalConnection = stateMachine[FINISHED_SIGNAL]:Connect(function(...) + receivedParameters = { ... } + task.cancel(timeoutThread) + coroutine.resume(mainThread, false) + end) + + stateMachine:handle(FINISH_EVENT, table.unpack(variadicArgs)) + + local didTimeOut = coroutine.yield(mainThread) + signalConnection:Disconnect() + expect(didTimeOut).toBe(false) + expect(#receivedParameters).toBe(1) + expect(receivedParameters[1]).toBe(initialState) + expect(stateMachine._currentState).toBe(FINISH_STATE) + end) + end) + end) + + describe("should invoke callbacks", function() + it("at the correct time", function() + local initialState = X_STATE + local mainThread = coroutine.running() + local timeoutThreadBefore, timeoutThreadAfter + local firedSignals = {} + local eventsByName = { + [FINISH_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = function() + task.cancel(timeoutThreadBefore) + coroutine.resume(mainThread, "beforeAsync") + return FINISH_STATE + end, + afterAsync = function() + task.cancel(timeoutThreadAfter) + coroutine.resume(mainThread, "afterAsync") + end, + }, + }, + }, + } + + local stateMachine = StateMachine.new(initialState, eventsByName) + local signalConnections = {} + + stateMachine:handle(FINISH_EVENT) + + table.insert( + signalConnections, + stateMachine[BEFORE_EVENT_SIGNAL]:Connect(function() + table.insert(firedSignals, BEFORE_EVENT_SIGNAL) + end) + ) + + table.insert( + signalConnections, + stateMachine[LEAVING_STATE_SIGNAL]:Connect(function() + table.insert(firedSignals, LEAVING_STATE_SIGNAL) + end) + ) + + table.insert( + signalConnections, + stateMachine[STATE_ENTERED_SIGNAL]:Connect(function() + table.insert(firedSignals, STATE_ENTERED_SIGNAL) + end) + ) + + table.insert( + signalConnections, + stateMachine[AFTER_EVENT_SIGNAL]:Connect(function() + table.insert(firedSignals, AFTER_EVENT_SIGNAL) + end) + ) + + table.insert( + signalConnections, + stateMachine[FINISHED_SIGNAL]:Connect(function() + table.insert(firedSignals, FINISHED_SIGNAL) + end) + ) + + local timeout = 0.5 + timeoutThreadBefore = task.delay(timeout, function() + for _, signalConnection in signalConnections do + signalConnection:Disconnect() + end + coroutine.resume(mainThread, `timeout waiting {timeout} seconds for beforeAsync invocation`) + end) + local invokedCallback = coroutine.yield() + expect(invokedCallback).toBe("beforeAsync") + local expectedFiredSignals = { BEFORE_EVENT_SIGNAL } + expect(firedSignals[1]).toBe(expectedFiredSignals[1]) + expect(#firedSignals).toBe(#expectedFiredSignals) + + timeoutThreadAfter = task.delay(timeout, function() + coroutine.resume(mainThread, `timeout waiting {timeout} seconds for afterAsync invocation`) + end) + invokedCallback = coroutine.yield() + for _, signalConnection in signalConnections do + signalConnection:Disconnect() + end + expect(invokedCallback).toBe("afterAsync") + expectedFiredSignals = { + BEFORE_EVENT_SIGNAL, + LEAVING_STATE_SIGNAL, + STATE_ENTERED_SIGNAL, + } + for index, expectedFiredSignal in expectedFiredSignals do + expect(firedSignals[index]).toBe(expectedFiredSignal) + end + expect(#firedSignals).toBe(#expectedFiredSignals) + end) + + it("with the correct parameters and state", function() + local initialState = X_STATE + local variadicArgs = { "test", false, nil, 3.5 } + local timeout = 0.5 + local timeoutThread + local mainThread = coroutine.running() + local handledEventName = FINISH_EVENT + local expectedAfterState = FINISH_STATE + local receivedParameters + local stateMachine + local eventsByName = { + [FINISH_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = function(...) + receivedParameters = { ... } + task.cancel(timeoutThread) + coroutine.resume(mainThread, "beforeAsync") + return FINISH_STATE + end, + afterAsync = function(...) + receivedParameters = { ... } + task.cancel(timeoutThread) + coroutine.resume(mainThread, "afterAsync") + end, + }, + }, + }, + } + + stateMachine = StateMachine.new(initialState, eventsByName) + + -- Test beforeAsync + timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, `timeout waiting {timeout} seconds for beforeAsync invocation`) + end) + + stateMachine:handle(handledEventName, table.unpack(variadicArgs)) + + local invokedCallback = coroutine.yield(mainThread) + expect(invokedCallback).toBe("beforeAsync") + for index, variadicArg in variadicArgs do + expect(receivedParameters[index]).toBe(variadicArg) + end + expect(#receivedParameters).toBe(#variadicArgs) + expect(stateMachine._currentState).toBe(initialState) + + -- Test afterAsync + timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, `timeout waiting {timeout} seconds for afterAsync invocation`) + end) + + local invokedCallback = coroutine.yield(mainThread) + expect(invokedCallback).toBe("afterAsync") + for index, variadicArg in variadicArgs do + expect(receivedParameters[index]).toBe(variadicArg) + end + expect(#receivedParameters).toBe(#variadicArgs) + expect(stateMachine._currentState).toBe(expectedAfterState) + end) + end) + + it("should queue and process async events in FIFO order", function() + local initialState = X_STATE + local timeout = 0.5 + local mainThread = coroutine.running() + local orderedHandledEvents = { + TO_Y_EVENT, + TO_X_EVENT, + FINISH_EVENT, + } + local actualHandledEvents = {} + local eventsByName = { + [TO_Y_EVENT] = { + canBeFinal = false, + from = { + [X_STATE] = { + beforeAsync = TO_Y_HANDLER, + afterAsync = function() + table.insert(actualHandledEvents, TO_Y_EVENT) + task.wait() + end, + }, + }, + }, + [TO_X_EVENT] = { + canBeFinal = false, + from = { + [Y_STATE] = { + beforeAsync = TO_X_HANDLER, + afterAsync = function() + table.insert(actualHandledEvents, TO_X_EVENT) + task.wait() + end, + }, + }, + }, + [FINISH_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = FINISH_HANDLER, + afterAsync = function() + table.insert(actualHandledEvents, FINISH_EVENT) + task.wait() + end, + }, + }, + }, + } + + local stateMachine = StateMachine.new(initialState, eventsByName) + + -- Queue events + for _, handledEventName in ipairs(orderedHandledEvents) do + stateMachine:handle(handledEventName) + end + + local timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, `timeout waiting {timeout} seconds for finished signal`) + end) + + local signalConnection = stateMachine[FINISHED_SIGNAL]:Connect(function() + task.cancel(timeoutThread) + coroutine.resume(mainThread, FINISHED_SIGNAL) + end) + + local invokedCallback = coroutine.yield(mainThread) + signalConnection:Disconnect() + expect(invokedCallback).toBe(FINISHED_SIGNAL) + for index, expectedHandledEventName in orderedHandledEvents do + expect(actualHandledEvents[index]).toBe(expectedHandledEventName) + end + expect(#actualHandledEvents).toBe(#orderedHandledEvents) + end) + + it("should error if called after the machine finished", function() + local eventsByName = { + [FINISH_EVENT] = { + canBeFinal = true, + from = { + [X_STATE] = { + beforeAsync = FINISH_HANDLER, + }, + }, + }, + } + + local stateMachine = StateMachine.new(X_STATE, eventsByName) + local mainThread = coroutine.running() + local logger = stateMachine:getLogger() + + local timeout = 0.5 + local timeoutMessage = `timeout waiting {timeout} seconds for finished or error message` + local timeoutThread = task.delay(timeout, function() + coroutine.resume(mainThread, timeoutMessage) + end) + + stateMachine:handle(FINISH_EVENT) + stateMachine.finished:Wait() + expect(stateMachine._currentState).toBe(FINISH_STATE) + + logger:addHandler(logger.LogLevel.Error, function(level: Logger.LogLevel, name: string, message: string) + if level ~= logger.LogLevel.Error then + return + end + + if coroutine.status(timeoutThread) ~= "suspended" then + return + end + + task.cancel(timeoutThread) + coroutine.resume(mainThread, message) + return logger.HandlerResult.Sink + end) + + stateMachine:handle(FINISH_EVENT) + local errorMessage = coroutine.yield() + expect(errorMessage == timeoutMessage).never.toBe(true) + expect(errorMessage).toEqual( + expect.stringContaining(`Attempt to process event {FINISH_EVENT} after the state machine already finished`) + ) end) end) + +-- Placeholder +-- describe("getState", function() +-- it("should return the current state correctly", function() +-- local initialState = "A" +-- local eventsByName = { +-- toB = { +-- canBeFinal = false, +-- from = { +-- A = { +-- beforeAsync = TO_Y_HANDLER, +-- }, +-- }, +-- }, +-- } + +-- local stateMachine = StateMachine.new(initialState, eventsByName) + +-- -- Test getting the current state +-- local state = stateMachine:getState() +-- expect(state).toBe(initialState) + +-- -- Test getting the state after a transition +-- stateMachine:handle("toB") +-- state = stateMachine:getState() +-- expect(state).toBe("B") +-- end) +-- end) + +-- Placeholder +-- describe("getValidEvents", function() +-- it("should return valid events correctly", function() +-- local initialState = "A" +-- local eventsByName = { +-- toB = { +-- canBeFinal = false, +-- from = { +-- A = { +-- beforeAsync = TO_Y_HANDLER, +-- }, +-- }, +-- }, +-- toA = { +-- canBeFinal = false, +-- from = { +-- B = { +-- beforeAsync = TO_X_HANDLER, +-- }, +-- }, +-- }, +-- } + +-- local stateMachine = StateMachine.new(initialState, eventsByName) + +-- -- Test getting valid events for the initial state +-- local validEvents = stateMachine:getValidEvents() +-- expect(#validEvents).toBe(1) +-- expect(validEvents[1]).toBe("toB") + +-- -- Test getting valid events after a transition +-- stateMachine:handle("toB") +-- validEvents = stateMachine:getValidEvents() +-- expect(#validEvents).toBe(1) +-- expect(validEvents[1]).toBe("toA") +-- end) +-- end) + +-- Done +-- describe("_isDebugEnabled", function() +-- it("should default to false", function() +-- local eventsByName = { +-- [TO_X_EVENT] = { +-- canBeFinal = true, +-- from = { +-- [X_STATE] = { +-- beforeAsync = TO_X_HANDLER, +-- }, +-- }, +-- }, +-- } + +-- local stateMachine = StateMachine.new(X_STATE, eventsByName) +-- expect(stateMachine._isDebugEnabled).toBe(false) +-- end) + +-- describe("setter", function() +-- it("should error given a bad type", function() +-- local eventsByName = { +-- [TO_X_EVENT] = { +-- canBeFinal = true, +-- from = { +-- [X_STATE] = { +-- beforeAsync = TO_X_HANDLER, +-- }, +-- }, +-- }, +-- } + +-- local stateMachine = StateMachine.new(X_STATE, eventsByName) + +-- local badTypes = { +-- 1, +-- "bad", +-- nil, +-- {}, +-- } + +-- for _, badType in badTypes do +-- expect(function() +-- stateMachine:setDebugEnabled(badType) +-- end).toThrow(`boolean expected, got {typeof(badType)}`) +-- end +-- end) + +-- it("should set the value correctly", function() +-- local eventsByName = { +-- [TO_X_EVENT] = { +-- canBeFinal = true, +-- from = { +-- [X_STATE] = { +-- beforeAsync = TO_X_HANDLER, +-- }, +-- }, +-- }, +-- } + +-- local stateMachine = StateMachine.new(X_STATE, eventsByName) + +-- stateMachine:setDebugEnabled(true) +-- expect(stateMachine._isDebugEnabled).toBe(true) + +-- stateMachine:setDebugEnabled(false) +-- expect(stateMachine._isDebugEnabled).toBe(false) +-- end) +-- end) +-- end) + +-- Placeholder +-- describe("destroy", function() +-- it("should destroy correctly", function() +-- local initialState = "A" +-- local eventsByName = { +-- toB = { +-- canBeFinal = false, +-- from = { +-- A = { +-- beforeAsync = TO_Y_HANDLER, +-- }, +-- }, +-- }, +-- } + +-- local stateMachine = StateMachine.new(initialState, eventsByName) + +-- -- Test destroying the state machine +-- stateMachine:destroy() +-- expect(stateMachine._isDestroyed).toBe(true) +-- expect(stateMachine[BEFORE_EVENT_SIGNAL]:getConnectionCount()).toBe(0) +-- expect(stateMachine[LEAVING_STATE_SIGNAL]:getConnectionCount()).toBe(0) +-- expect(stateMachine[STATE_ENTERED_SIGNAL]:getConnectionCount()).toBe(0) +-- expect(stateMachine[AFTER_EVENT_SIGNAL]:getConnectionCount()).toBe(0) +-- expect(stateMachine[FINISHED_SIGNAL]:getConnectionCount()).toBe(0) +-- end) +-- end) + +-- return function() +-- local StateMachine = require(script.Parent.Parent.StateMachine) + +-- local function createTestMachine() +-- local states = { +-- ["A"] = { +-- ["toB"] = { +-- canBeFinal = false, +-- from = { +-- ["A"] = { +-- beforeAsync = function() +-- wait(0.1) +-- return "B" +-- end, +-- }, +-- }, +-- }, +-- }, +-- ["B"] = { +-- ["toC"] = { +-- canBeFinal = false, +-- from = { +-- ["B"] = { +-- beforeAsync = function() +-- wait(0.1) +-- return "C" +-- end, +-- }, +-- }, +-- }, +-- ["toA"] = { +-- canBeFinal = false, +-- from = { +-- ["B"] = { +-- beforeAsync = function() +-- wait(0.1) +-- return "A" +-- end, +-- }, +-- }, +-- }, +-- }, +-- ["C"] = { +-- ["toA"] = { +-- canBeFinal = true, +-- from = { +-- ["C"] = { +-- beforeAsync = function() +-- wait(0.1) +-- return nil +-- end, +-- }, +-- }, +-- }, +-- }, +-- } + +-- local machine = StateMachine.new("A", states) +-- machine:setDebugEnabled(true) +-- return machine +-- end + +-- describe("new", function() +-- it("should create a new StateMachine", function() +-- local machine = createTestMachine() +-- expect(machine).never.toBeNil() +-- end) + +-- it("should require an initial state", function() +-- expect(function() +-- StateMachine.new() +-- end).toThrow() +-- end) + +-- it("should require events", function() +-- expect(function() +-- StateMachine.new("A") +-- end).toThrow() +-- end) +-- end) + +-- describe("handle", function() +-- it("should process events", function() +-- local machine = createTestMachine() + +-- local events = {} +-- machine[FINISHED_SIGNAL]:Connect(function(state) +-- table.insert(events, state) +-- end) + +-- machine:handle("toB") +-- machine:handle("toC") +-- machine:handle("toA") + +-- wait(0.4) + +-- expect(events).never.toBeNil() +-- expect(events[1]).toBe("B") +-- end From ca65d60d3b43c5b31ee363a9c0b955e84e4d4861 Mon Sep 17 00:00:00 2001 From: BusyCityGuy <55513323+BusyCityGuy@users.noreply.github.com> Date: Tue, 14 May 2024 14:52:17 -0700 Subject: [PATCH 2/7] Missed uncommenting ReplicatedStorage --- src/TestService/Source/Tests/StateMachine.spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TestService/Source/Tests/StateMachine.spec.lua b/src/TestService/Source/Tests/StateMachine.spec.lua index b86d3dc..d917dde 100644 --- a/src/TestService/Source/Tests/StateMachine.spec.lua +++ b/src/TestService/Source/Tests/StateMachine.spec.lua @@ -1,7 +1,7 @@ --!nonstrict -- FIXME: Change to strict and fix type issues --- local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ReplicatedStorage = game:GetService("ReplicatedStorage") local TestService = game:GetService("TestService") local Freeze = require(TestService.Dependencies.Freeze) From d7613b686b8c474bf2c1e089eab3d7c77a9fde8b Mon Sep 17 00:00:00 2001 From: BusyCityGuy <55513323+BusyCityGuy@users.noreply.github.com> Date: Tue, 14 May 2024 14:53:24 -0700 Subject: [PATCH 3/7] Address selene warning --- .../Source/Tests/StateMachine.spec.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/TestService/Source/Tests/StateMachine.spec.lua b/src/TestService/Source/Tests/StateMachine.spec.lua index d917dde..6bb9e63 100644 --- a/src/TestService/Source/Tests/StateMachine.spec.lua +++ b/src/TestService/Source/Tests/StateMachine.spec.lua @@ -721,13 +721,13 @@ describe("handle", function() coroutine.resume(mainThread, `timeout waiting {timeout} seconds for afterAsync invocation`) end) - local invokedCallback = coroutine.yield(mainThread) - expect(invokedCallback).toBe("afterAsync") - for index, variadicArg in variadicArgs do - expect(receivedParameters[index]).toBe(variadicArg) - end - expect(#receivedParameters).toBe(#variadicArgs) - expect(stateMachine._currentState).toBe(expectedAfterState) + -- local invokedCallback = coroutine.yield(mainThread) + -- expect(invokedCallback).toBe("afterAsync") + -- for index, variadicArg in variadicArgs do + -- expect(receivedParameters[index]).toBe(variadicArg) + -- end + -- expect(#receivedParameters).toBe(#variadicArgs) + -- expect(stateMachine._currentState).toBe(expectedAfterState) end) end) @@ -831,7 +831,7 @@ describe("handle", function() stateMachine.finished:Wait() expect(stateMachine._currentState).toBe(FINISH_STATE) - logger:addHandler(logger.LogLevel.Error, function(level: Logger.LogLevel, name: string, message: string) + logger:addHandler(logger.LogLevel.Error, function(level: Logger.LogLevel, _name: string, message: string) if level ~= logger.LogLevel.Error then return end From c236ea30a3af4a5fd84c68570a1c5e04fd4e2b92 Mon Sep 17 00:00:00 2001 From: BusyCityGuy <55513323+BusyCityGuy@users.noreply.github.com> Date: Tue, 14 May 2024 14:54:35 -0700 Subject: [PATCH 4/7] `plural` is unused for now --- src/TestService/Source/Tests/StateMachine.spec.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TestService/Source/Tests/StateMachine.spec.lua b/src/TestService/Source/Tests/StateMachine.spec.lua index 6bb9e63..8f27d5f 100644 --- a/src/TestService/Source/Tests/StateMachine.spec.lua +++ b/src/TestService/Source/Tests/StateMachine.spec.lua @@ -52,9 +52,9 @@ local TO_X_HANDLER = to(X_STATE) local TO_Y_HANDLER = to(Y_STATE) local FINISH_HANDLER = to(FINISH_STATE) -local function plural(count: number) - return if count == 1 then "" else "s" -end +-- local function plural(count: number) +-- return if count == 1 then "" else "s" +-- end -- TODO: Evaluate each test to see if it could be done better in Jest. These are currently just translated directly from TestEZ. -- Done From 3f6de1986743d272351549c7654df1bdce645148 Mon Sep 17 00:00:00 2001 From: BusyCityGuy <55513323+BusyCityGuy@users.noreply.github.com> Date: Sat, 18 May 2024 20:56:13 -0700 Subject: [PATCH 5/7] rename `runTests` -> `test`, easier script name to type --- CONTRIBUTING.md | 2 +- lune/Context/Debug.lua | 2 +- lune/Utils/Runtime.lua | 2 +- lune/{runTests.lua => test.lua} | 6 +++--- scripts/test.sh | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename lune/{runTests.lua => test.lua} (98%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 071f036..c71ccad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,5 +146,5 @@ You need to set the `FFlagEnableLoadModule` value to `true`. Be sure to restart | Method | Instructions | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| CLI (recommended) | `lune run runTests` | +| CLI (recommended) | `lune run test` | | Roblox Studio | Open the test place file `StateMachine-Test.rbxl` [built in the above step](#build-the-project) in Roblox Studio and run the place (server only). The output widget will show the test results. | diff --git a/lune/Context/Debug.lua b/lune/Context/Debug.lua index 7cb4315..4268a0a 100644 --- a/lune/Context/Debug.lua +++ b/lune/Context/Debug.lua @@ -5,7 +5,7 @@ On Roblox, it's locked behind a feature flag FFlagDebugLoadModule, which is not enabled by default. In Lune, it doesn't exist at all, so this module creates the interface for it but is not implemented. The _loader function needs to be overridden to be usable. - In this project, the `lune/runTests.lua` script implements and sets the _loader function. + In this project, the `lune/test.lua` script implements and sets the _loader function. --]] local Debug diff --git a/lune/Utils/Runtime.lua b/lune/Utils/Runtime.lua index ab5f1db..21b36f1 100644 --- a/lune/Utils/Runtime.lua +++ b/lune/Utils/Runtime.lua @@ -2,7 +2,7 @@ --[[ Provides a connection to a loop that runs every frame. This is used - in the custom Heartbeat implementation in lune/runTests.lua + in the custom Heartbeat implementation in lune/test.lua --]] local task = require("@lune/task") diff --git a/lune/runTests.lua b/lune/test.lua similarity index 98% rename from lune/runTests.lua rename to lune/test.lua index c72eca4..4e92282 100644 --- a/lune/runTests.lua +++ b/lune/test.lua @@ -7,7 +7,7 @@ Since the parent folder is named `lune`, the `lune` cli will automatically look in this directory for scripts to run. Usage (from project directory): - lune run runTests [project.json] + lune run test [project.json] --]] local fs = require("@lune/fs") @@ -22,7 +22,7 @@ local DateTime = require("Context/DateTime") local Debug = require("Context/Debug") local Runtime = require("Utils/Runtime") --- DEPENDENTS: [runTests.lua, Jest] +-- DEPENDENTS: [test.lua, Jest] local ReducedInstance = require("Utils/ReducedInstance") type RojoProject = { @@ -120,7 +120,7 @@ local gameWithContext = setmetatable({ end, }, { __index = game }) --- DEPENDENTS: [runTests.lua, Jest] +-- DEPENDENTS: [test.lua, Jest] local function loadScript(script: roblox.Instance): (((...any) -> ...any)?, string?) script = ReducedInstance.once(script) if not script:IsA("LuaSourceContainer") then diff --git a/scripts/test.sh b/scripts/test.sh index decbf9e..f7d738a 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -4,6 +4,6 @@ set -e echo "Starting test runner..." -$HOME/.aftman/bin/lune run runTests +$HOME/.aftman/bin/lune run test echo "Test runner complete!" \ No newline at end of file From 4d1e9fd1a8062e8e06507e27099495bd31bab42a Mon Sep 17 00:00:00 2001 From: BusyCityGuy <55513323+BusyCityGuy@users.noreply.github.com> Date: Sat, 18 May 2024 23:30:57 -0700 Subject: [PATCH 6/7] Add lune commands to easily run ci steps locally --- .gitignore | 2 +- CONTRIBUTING.md | 30 ++++++ lune/analyze.lua | 93 +++++++++++++++++++ lune/ci.lua | 62 +++++++++++++ lune/formatCheck.lua | 62 +++++++++++++ lune/formatFix.lua | 63 +++++++++++++ lune/lint.lua | 60 ++++++++++++ scripts/analyze.sh | 4 +- scripts/formatCheck.sh | 2 +- scripts/lint.sh | 2 +- scripts/sourcemap.sh | 2 +- .../Source/Tests/StateMachine.spec.lua | 2 +- 12 files changed, 377 insertions(+), 7 deletions(-) create mode 100644 lune/analyze.lua create mode 100644 lune/ci.lua create mode 100644 lune/formatCheck.lua create mode 100644 lune/formatFix.lua create mode 100644 lune/lint.lua diff --git a/.gitignore b/.gitignore index c7c40ed..10f5886 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,5 @@ globaltypes.d.lua Packages DevPackages -sourcemap.json +*sourcemap.json settings.json \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c71ccad..42c27d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,3 +148,33 @@ You need to set the `FFlagEnableLoadModule` value to `true`. Be sure to restart | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | CLI (recommended) | `lune run test` | | Roblox Studio | Open the test place file `StateMachine-Test.rbxl` [built in the above step](#build-the-project) in Roblox Studio and run the place (server only). The output widget will show the test results. | + + +### Continuous Integration (CI) + +CI checks are set up to run on pull requests. These checks must pass before merging, including: + +1. `lint` with selene +1. `format check` with StyLua +1. `analyze` with luau-lsp +1. `test` with jest running in Lune + +#### Running CI Locally + +To run the same CI checks locally that would run on GitHub, a number of Lune scripts are provided. + +From the project directory, you can run the following: + +> lune run ci + +This will run all the same checks that would run on GitHub. + +Alternatively, you can run individual steps yourself: + +> lune run lint + +> lune run formatCheck + +> lune run analyze + +> lune run test \ No newline at end of file diff --git a/lune/analyze.lua b/lune/analyze.lua new file mode 100644 index 0000000..ae37fe4 --- /dev/null +++ b/lune/analyze.lua @@ -0,0 +1,93 @@ +--!strict + +--[[ + Analyzes all the same paths that get analyzed in the CI pipeline. + + Since the parent folder is named `lune`, the `lune` cli will automatically look in this directory for scripts to run. + + Usage (from project directory): + lune run analyze +--]] + +local process = require("@lune/process") +local stdio = require("@lune/stdio") +local task = require("@lune/task") + +local ANALYZE_PATHS: { { path: string, project: string?, sourceMap: string? } } = { + { + path = "src/StateMachine", + project = "default.project.json", + sourceMap = "stateMachineSourcemap.json", + }, + { + path = "src/TestService", + project = "test.project.json", + sourceMap = "testSourcemap.json", + }, + { + path = "lune", + project = nil, + sourceMap = nil, + }, +} + +local NUM_EXPECTED_TASKS = #ANALYZE_PATHS + +local numErrors = 0 +local numTasksCompleted = 0 +local mainThread = coroutine.running() + +local function buildSourceMap(projectFilePath: string, sourceMapFilePath: string) + local proc = process.spawn("./scripts/sourcemap.sh", { projectFilePath, sourceMapFilePath }) + print(proc.stdout) + assert(proc.ok, proc.stderr) +end + +local function analyzePath(path: string, sourceMap: string?) + local proc + if sourceMap then + proc = process.spawn("./scripts/analyze.sh", { sourceMap, path }) + else + proc = process.spawn("./scripts/analyze.sh", { path }) + end + + print(proc.stdout) + assert(proc.ok, proc.stderr) +end + +local function analyzeAllPaths() + print(`Analyzing {NUM_EXPECTED_TASKS} paths:`) + for _, path in ipairs(ANALYZE_PATHS) do + task.spawn(function() + local success, errorMessage: string? = pcall(function() + if path.project and path.sourceMap then + buildSourceMap(path.project, path.sourceMap) + end + analyzePath(path.path, path.sourceMap) + end) + + if not success then + stdio.ewrite(errorMessage :: string) + numErrors += 1 + end + + numTasksCompleted += 1 + if numTasksCompleted == NUM_EXPECTED_TASKS then + coroutine.resume(mainThread) + end + end) + end + + if numTasksCompleted < NUM_EXPECTED_TASKS then + coroutine.yield() + end + + if numErrors > 0 then + stdio.ewrite(`{numErrors} of {NUM_EXPECTED_TASKS} paths FAILED analysis\n`) + process.exit(1) + end + + stdio.write(`All ({NUM_EXPECTED_TASKS}) paths analyzed successfully\n`) +end + +analyzeAllPaths() diff --git a/lune/ci.lua b/lune/ci.lua new file mode 100644 index 0000000..c090bb9 --- /dev/null +++ b/lune/ci.lua @@ -0,0 +1,62 @@ +--!strict + +--[[ + Runs all the same checks that CI steps run, including analysis, format check, linting, and testing. + + Since the parent folder is named `lune`, the `lune` cli will automatically look in this directory for scripts to run. + + Usage (from project directory): + lune run ci +--]] + +local process = require("@lune/process") +local stdio = require("@lune/stdio") +local task = require("@lune/task") + +local LUNE_COMMANDS = { + "lint", + "formatCheck", + "analyze", + "test", +} + +local NUM_EXPECTED_TASKS = #LUNE_COMMANDS + +local numErrors = 0 +local numTasksCompleted = 0 +local mainThread = coroutine.running() + +local function runLuneCommand(command: string) + local home = process.env.HOME + local proc = process.spawn(`{home}/.aftman/bin/lune`, { "run", command }) + print(proc.stdout) + if not proc.ok then + stdio.ewrite(proc.stderr) + numErrors += 1 + end + + numTasksCompleted += 1 + if numTasksCompleted == NUM_EXPECTED_TASKS then + coroutine.resume(mainThread) + end +end + +local function runAllLuneCommands() + print(`Fixing format for {NUM_EXPECTED_TASKS} paths:`) + for _, path in ipairs(LUNE_COMMANDS) do + task.spawn(runLuneCommand, path) + end + + if numTasksCompleted < NUM_EXPECTED_TASKS then + coroutine.yield() + end + + if numErrors > 0 then + stdio.ewrite(`{numErrors} of {NUM_EXPECTED_TASKS} commands FAILED\n`) + process.exit(1) + end + + stdio.write(`All ({NUM_EXPECTED_TASKS}) commands succeeded\n`) +end + +runAllLuneCommands() diff --git a/lune/formatCheck.lua b/lune/formatCheck.lua new file mode 100644 index 0000000..975a559 --- /dev/null +++ b/lune/formatCheck.lua @@ -0,0 +1,62 @@ +--!strict + +--[[ + Checks the format for all the same paths that get checked in the CI pipeline. + This is just a check that prints problems to the output. + To fix the format, run `lune run formatFix` instead. + + Since the parent folder is named `lune`, the `lune` cli will automatically look in this directory for scripts to run. + + Usage (from project directory): + lune run formatCheck +--]] + +local process = require("@lune/process") +local stdio = require("@lune/stdio") +local task = require("@lune/task") + +local FORMAT_PATHS = { + "src/StateMachine", + "src/TestService", + "lune", +} + +local NUM_EXPECTED_TASKS = #FORMAT_PATHS + +local numErrors = 0 +local numTasksCompleted = 0 +local mainThread = coroutine.running() + +local function checkFormatForPath(path: string) + local proc = process.spawn("./scripts/formatCheck.sh", { path }) + print(proc.stdout) + if not proc.ok then + stdio.ewrite(proc.stderr) + numErrors += 1 + end + + numTasksCompleted += 1 + if numTasksCompleted == NUM_EXPECTED_TASKS then + coroutine.resume(mainThread) + end +end + +local function formatCheckAllPaths() + print(`Checking format for {NUM_EXPECTED_TASKS} paths:`) + for _, path in ipairs(FORMAT_PATHS) do + task.spawn(checkFormatForPath, path) + end + + if numTasksCompleted < NUM_EXPECTED_TASKS then + coroutine.yield() + end + + if numErrors > 0 then + stdio.ewrite(`{numErrors} of {NUM_EXPECTED_TASKS} paths FAILED formatting checks\n`) + process.exit(1) + end + + stdio.write(`All ({NUM_EXPECTED_TASKS}) paths passed format checks successfully\n`) +end + +formatCheckAllPaths() diff --git a/lune/formatFix.lua b/lune/formatFix.lua new file mode 100644 index 0000000..d326636 --- /dev/null +++ b/lune/formatFix.lua @@ -0,0 +1,63 @@ +--!strict + +--[[ + Fixes the format for all the same paths that get fixed in the CI pipeline. + This changes the files in place. + To just check the format, run `lune run formatCheck` instead. + + Since the parent folder is named `lune`, the `lune` cli will automatically look in this directory for scripts to run. + + Usage (from project directory): + lune run formatFix +--]] + +local process = require("@lune/process") +local stdio = require("@lune/stdio") +local task = require("@lune/task") + +local FORMAT_PATHS = { + "src/StateMachine", + "src/TestService", + "lune", +} + +local NUM_EXPECTED_TASKS = #FORMAT_PATHS + +local numErrors = 0 +local numTasksCompleted = 0 +local mainThread = coroutine.running() + +local function fixFormatForPath(path: string) + local home = process.env.HOME + local proc = process.spawn(`{home}/.aftman/bin/stylua`, { path }) + print(proc.stdout) + if not proc.ok then + stdio.ewrite(proc.stderr) + numErrors += 1 + end + + numTasksCompleted += 1 + if numTasksCompleted == NUM_EXPECTED_TASKS then + coroutine.resume(mainThread) + end +end + +local function formatFixAllPaths() + print(`Fixing format for {NUM_EXPECTED_TASKS} paths:`) + for _, path in ipairs(FORMAT_PATHS) do + task.spawn(fixFormatForPath, path) + end + + if numTasksCompleted < NUM_EXPECTED_TASKS then + coroutine.yield() + end + + if numErrors > 0 then + stdio.ewrite(`{numErrors} of {NUM_EXPECTED_TASKS} paths FAILED to get format fixed\n`) + process.exit(1) + end + + stdio.write(`All ({NUM_EXPECTED_TASKS}) paths have fixed formats\n`) +end + +formatFixAllPaths() diff --git a/lune/lint.lua b/lune/lint.lua new file mode 100644 index 0000000..5879425 --- /dev/null +++ b/lune/lint.lua @@ -0,0 +1,60 @@ +--!strict + +--[[ + Lints all the same paths that get linted in the CI pipeline. + + Since the parent folder is named `lune`, the `lune` cli will automatically look in this directory for scripts to run. + + Usage (from project directory): + lune run lint +--]] + +local process = require("@lune/process") +local stdio = require("@lune/stdio") +local task = require("@lune/task") + +local LINT_PATHS = { + "src/StateMachine", + "src/TestService", + "lune", +} + +local NUM_EXPECTED_TASKS = #LINT_PATHS + +local numErrors = 0 +local numTasksCompleted = 0 +local mainThread = coroutine.running() + +local function lintPath(path: string) + local proc = process.spawn("./scripts/lint.sh", { path }) + print(proc.stdout) + if not proc.ok then + stdio.ewrite(proc.stderr) + numErrors += 1 + end + + numTasksCompleted += 1 + if numTasksCompleted == NUM_EXPECTED_TASKS then + coroutine.resume(mainThread) + end +end + +local function lintAllPaths() + print(`Linting {NUM_EXPECTED_TASKS} paths:`) + for _, path in ipairs(LINT_PATHS) do + task.spawn(lintPath, path) + end + + if numTasksCompleted < NUM_EXPECTED_TASKS then + coroutine.yield() + end + + if numErrors > 0 then + stdio.ewrite(`{numErrors} of {NUM_EXPECTED_TASKS} paths FAILED linting\n`) + process.exit(1) + end + + stdio.write(`All ({NUM_EXPECTED_TASKS}) paths linted successfully\n`) +end + +lintAllPaths() diff --git a/scripts/analyze.sh b/scripts/analyze.sh index 01108e5..859d6dc 100755 --- a/scripts/analyze.sh +++ b/scripts/analyze.sh @@ -19,6 +19,7 @@ if [ -n "$2" ]; then --sourcemap "$1" \ --ignore "*Packages/**" \ "$2" + echo "Analysis of $2 complete!" else echo "Beginning analysis on $1 with settings from $SETTINGS_FILE and global types from $TYPES_FILE..." $HOME/.aftman/bin/luau-lsp analyze \ @@ -26,6 +27,5 @@ else --definitions=$TYPES_FILE \ --ignore "*Packages/**" \ "$1" + echo "Analysis of $1 complete!" fi - -echo "Analysis complete!" \ No newline at end of file diff --git a/scripts/formatCheck.sh b/scripts/formatCheck.sh index b5aa61f..87f833e 100755 --- a/scripts/formatCheck.sh +++ b/scripts/formatCheck.sh @@ -6,4 +6,4 @@ echo "Beginning format check on $1..." $HOME/.aftman/bin/stylua --check $1 -echo "Format check complete!" \ No newline at end of file +echo "Format check on $1 complete!" \ No newline at end of file diff --git a/scripts/lint.sh b/scripts/lint.sh index 0a6cc0f..7412914 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -6,4 +6,4 @@ echo "Beginning linting on $1..." $HOME/.aftman/bin/selene $1 -echo "Linting complete!" \ No newline at end of file +echo "Linting $1 complete!" \ No newline at end of file diff --git a/scripts/sourcemap.sh b/scripts/sourcemap.sh index 2b6025e..853b0c6 100755 --- a/scripts/sourcemap.sh +++ b/scripts/sourcemap.sh @@ -6,4 +6,4 @@ echo "Creating sourcemap from $1 to $2..." $HOME/.aftman/bin/rojo sourcemap $1 --output $2 -echo "Sourcemap created!" \ No newline at end of file +echo "Sourcemap $2 created!" \ No newline at end of file diff --git a/src/TestService/Source/Tests/StateMachine.spec.lua b/src/TestService/Source/Tests/StateMachine.spec.lua index 8f27d5f..fa6d5ac 100644 --- a/src/TestService/Source/Tests/StateMachine.spec.lua +++ b/src/TestService/Source/Tests/StateMachine.spec.lua @@ -675,7 +675,7 @@ describe("handle", function() local timeoutThread local mainThread = coroutine.running() local handledEventName = FINISH_EVENT - local expectedAfterState = FINISH_STATE + -- local expectedAfterState = FINISH_STATE local receivedParameters local stateMachine local eventsByName = { From 0f6dac21507231747a51031f198a15440cb52cd6 Mon Sep 17 00:00:00 2001 From: BusyCityGuy <55513323+BusyCityGuy@users.noreply.github.com> Date: Sat, 18 May 2024 23:32:40 -0700 Subject: [PATCH 7/7] Undo changes to spec file for this branch --- .../Source/Tests/StateMachine.spec.lua | 1132 +---------------- 1 file changed, 35 insertions(+), 1097 deletions(-) diff --git a/src/TestService/Source/Tests/StateMachine.spec.lua b/src/TestService/Source/Tests/StateMachine.spec.lua index fa6d5ac..cf65a87 100644 --- a/src/TestService/Source/Tests/StateMachine.spec.lua +++ b/src/TestService/Source/Tests/StateMachine.spec.lua @@ -1,1119 +1,57 @@ ---!nonstrict --- FIXME: Change to strict and fix type issues +--!strict -local ReplicatedStorage = game:GetService("ReplicatedStorage") +-- local ReplicatedStorage = game:GetService("ReplicatedStorage") local TestService = game:GetService("TestService") -local Freeze = require(TestService.Dependencies.Freeze) local JestGlobals = require(TestService.Dependencies.JestGlobals) -local Logger = require(ReplicatedStorage.Source.StateMachine.Modules.Logger) -local Signal = require(ReplicatedStorage.Source.StateMachine.Modules.Signal) -local StateMachine = require(ReplicatedStorage.Source.StateMachine) +-- local Freeze = require(TestService.Dependencies.Freeze) +-- local Logger = require(ReplicatedStorage.Source.StateMachine.Modules.Logger) +-- local StateMachine = require(ReplicatedStorage.Source.StateMachine) -- Shortening things is generally bad practice, but this greatly improves readability of tests -local Dict = Freeze.Dictionary +-- local Dict = Freeze.Dictionary local it = JestGlobals.it local expect = JestGlobals.expect local describe = JestGlobals.describe -local beforeEach = JestGlobals.beforeEach +-- local beforeEach = JestGlobals.beforeEach -- States -local X_STATE = "X_STATE" -local Y_STATE = "Y_STATE" -local FINISH_STATE = nil +-- local X_STATE = "X_STATE" +-- local Y_STATE = "Y_STATE" +-- local FINISH_STATE = nil -- Events -local TO_X_EVENT = "TO_X_EVENT" -local TO_Y_EVENT = "TO_Y_EVENT" -local FINISH_EVENT = "FINISH_EVENT" +-- local TO_X_EVENT = "TO_X_EVENT" +-- local TO_Y_EVENT = "TO_Y_EVENT" +-- local FINISH_EVENT = "FINISH_EVENT" -- Signals -local BEFORE_EVENT_SIGNAL = "beforeEvent" -local LEAVING_STATE_SIGNAL = "leavingState" -local STATE_ENTERED_SIGNAL = "stateEntered" -local AFTER_EVENT_SIGNAL = "afterEvent" -local FINISHED_SIGNAL = "finished" -local ORDERED_SIGNALS = { - BEFORE_EVENT_SIGNAL, - LEAVING_STATE_SIGNAL, - STATE_ENTERED_SIGNAL, - AFTER_EVENT_SIGNAL, - FINISHED_SIGNAL, -} +-- local BEFORE_EVENT_SIGNAL = "beforeEvent" +-- local LEAVING_STATE_SIGNAL = "leavingState" +-- local STATE_ENTERED_SIGNAL = "stateEntered" +-- local AFTER_EVENT_SIGNAL = "afterEvent" +-- local FINISHED_SIGNAL = "finished" +-- local ORDERED_SIGNALS = { +-- BEFORE_EVENT_SIGNAL, +-- LEAVING_STATE_SIGNAL, +-- STATE_ENTERED_SIGNAL, +-- AFTER_EVENT_SIGNAL, +-- FINISHED_SIGNAL, +-- } -- Transition handlers -local function to(state: string) - return function() - return state - end -end - -local TO_X_HANDLER = to(X_STATE) -local TO_Y_HANDLER = to(Y_STATE) -local FINISH_HANDLER = to(FINISH_STATE) - --- local function plural(count: number) --- return if count == 1 then "" else "s" +-- local function to(state: string) +-- return function() +-- return state +-- end -- end --- TODO: Evaluate each test to see if it could be done better in Jest. These are currently just translated directly from TestEZ. --- Done -describe("new", function() - describe("should error iff given bad parameter type for", function() - it("initial state", function() - local eventsByName = { - [TO_X_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local badTypes: { any } = { - 1, - true, - nil, - {}, - } - - local goodType = X_STATE - - for _, badType in badTypes do - expect(function() - StateMachine.new(badType, eventsByName) - end).toThrow("Bad tuple index #1") - end - - expect(function() - StateMachine.new(goodType, eventsByName) - end).never.toThrow() - end) - - it("events", function() - local goodEventsByName = { - [TO_X_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local badTypes: { any } = { - 1, - true, - nil, - { - "bad", - "type", - }, - } - - for _, badType in badTypes do - expect(function() - StateMachine.new(X_STATE, badType) - end).toThrow("Bad tuple index #2") - end - - expect(function() - StateMachine.new(X_STATE, goodEventsByName) - end).never.toThrow() - end) - - it("name", function() - local eventsByName = { - [TO_X_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local badTypes: { any } = { - 1, - true, - nil, - {}, - } - - local goodTypes: { any } = { - nil, - "good type", - "", - } - - for _, badType in badTypes do - expect(function() - StateMachine.new(X_STATE, eventsByName, badType) - end).toThrow("Bad tuple index #3") - end - - for _, goodType in goodTypes do - expect(function() - StateMachine.new(X_STATE, eventsByName, goodType) - end).never.toThrow() - end - end) - - it("log level", function() - local eventsByName = { - [TO_X_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local badTypes: { any } = { - 1, - true, - nil, - {}, - "bad type", - } - - local goodTypes: { nil | Logger.LogLevel } = { - nil, - StateMachine.Logger.LogLevel.Error, - StateMachine.Logger.LogLevel.Warn, - StateMachine.Logger.LogLevel.Info, - StateMachine.Logger.LogLevel.Debug, - } - - for _, badType in badTypes do - expect(function() - StateMachine.new(X_STATE, eventsByName, nil, badType) - end).toThrow("Bad tuple index #4") - end - - for _, goodType: nil | Logger.LogLevel in goodTypes do - expect(function() - StateMachine.new(X_STATE, eventsByName, nil, goodType) - end).never.toThrow() - end - end) - end) - - it("should return a new state machine", function() - local initialState = X_STATE - local eventsByName = { - [TO_X_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local stateMachine1 = StateMachine.new(initialState, eventsByName) - local stateMachine2 = StateMachine.new(initialState, eventsByName) - - expect(stateMachine1).toBeInstanceOf(StateMachine) - expect(stateMachine2).toBeInstanceOf(StateMachine) - expect(stateMachine1 == stateMachine2).toBe(false) - end) - - it("should set initial state", function() - local initialState = X_STATE - local eventsByName = { - [TO_X_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local stateMachine = StateMachine.new(initialState, eventsByName) - expect(stateMachine._currentState).toBe(initialState) - end) - - it("should set valid event names by state", function() - local eventsByName = { - [TO_Y_EVENT] = { - canBeFinal = false, - from = { - [X_STATE] = { - beforeAsync = TO_Y_HANDLER, - }, - [Y_STATE] = { - beforeAsync = TO_Y_HANDLER, - }, - }, - }, - [TO_X_EVENT] = { - canBeFinal = false, - from = { - [Y_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local stateMachine = StateMachine.new(X_STATE, eventsByName) - local validEventNamesFromX = stateMachine._validEventNamesByState[X_STATE] - local validEventNamesFromY = stateMachine._validEventNamesByState[Y_STATE] - - expect(validEventNamesFromX).toEqual(expect.any("table")) - expect(validEventNamesFromY).toEqual(expect.any("table")) - - expect(Dict.count(validEventNamesFromX)).toBe(1) - expect(Dict.includes(validEventNamesFromX, TO_Y_EVENT)).toBe(true) - - expect(Dict.count(validEventNamesFromY)).toBe(2) - expect(Dict.includes(validEventNamesFromY, TO_X_EVENT)).toBe(true) - expect(Dict.includes(validEventNamesFromY, TO_Y_EVENT)).toBe(true) - end) - - it("should set handlers by event name", function() - local eventsByName = { - [TO_Y_EVENT] = { - canBeFinal = false, - from = { - [X_STATE] = { - beforeAsync = TO_Y_HANDLER, - }, - [Y_STATE] = { - beforeAsync = TO_Y_HANDLER, - }, - }, - }, - [TO_X_EVENT] = { - canBeFinal = false, - from = { - [Y_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local stateMachine = StateMachine.new(X_STATE, eventsByName) - local handlers = stateMachine._handlersByEventName - - expect(Dict.count(handlers)).toBe(2) - expect(handlers[TO_X_EVENT]).toEqual(expect.any("function")) - expect(handlers[TO_Y_EVENT]).toEqual(expect.any("function")) - end) - - it("should make signals available", function() - local eventsByName = { - [TO_X_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local stateMachine = StateMachine.new(X_STATE, eventsByName) - - expect(stateMachine[BEFORE_EVENT_SIGNAL]).never.toBeNil() - expect(stateMachine[LEAVING_STATE_SIGNAL]).never.toBeNil() - expect(stateMachine[STATE_ENTERED_SIGNAL]).never.toBeNil() - expect(stateMachine[AFTER_EVENT_SIGNAL]).never.toBeNil() - expect(stateMachine[FINISHED_SIGNAL]).never.toBeNil() - end) -end) - --- Done -describe("handle", function() - it("should error given a bad event name", function() - local nonexistentEventName = "nonexistent" - local eventsByName = { - [TO_X_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - } - - local stateMachine = StateMachine.new(X_STATE, eventsByName) - - local badTypes: { any } = { - 1, - true, - nil, - {}, - } - - for _, badType in badTypes do - expect(function() - stateMachine:handle(badType) - end).toThrow(`string expected, got {typeof(badType)}`) - end - - expect(function() - stateMachine:handle(nonexistentEventName) - end).toThrow(`Invalid event name passed to handle: {nonexistentEventName}`) - end) - - describe("should fire signals", function() - it("in the correct order", function(_, done) - local initialState = X_STATE - local eventsByName = { - [FINISH_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = FINISH_HANDLER, - }, - }, - }, - } - - local stateMachine = StateMachine.new(initialState, eventsByName) - local resultSignalOrder: { string } = {} - local signalConnections: { Signal.SignalConnection } = {} - - -- Set up event connections - for _, signalName in ORDERED_SIGNALS do - local newSignalConnection = (stateMachine[signalName] :: Signal.ClassType):Connect(function(_, _) - table.insert(resultSignalOrder, signalName) - - if #resultSignalOrder == #ORDERED_SIGNALS then - for _, signalConnection in signalConnections do - signalConnection:Disconnect() - end - xpcall(function() - expect(resultSignalOrder).toEqual(ORDERED_SIGNALS) - done() - end, function(err) - done(err) - end) - end - end) - - table.insert(signalConnections, newSignalConnection) - end - - stateMachine:handle(FINISH_EVENT) - end, 500) - - describe(`with the correct parameters and state`, function() - local initialState = X_STATE - local variadicArgs = { "test", false, nil, 3.5 } - local timeout = 0.5 - local handledEventName = TO_Y_EVENT - local receivedParameters - local eventsByName = { - [TO_Y_EVENT] = { - canBeFinal = false, - from = { - [X_STATE] = { - beforeAsync = TO_Y_HANDLER, - }, - }, - }, - [TO_X_EVENT] = { - canBeFinal = false, - from = { - [Y_STATE] = { - beforeAsync = TO_X_HANDLER, - }, - }, - }, - [FINISH_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = FINISH_HANDLER, - }, - }, - }, - } - - local stateMachine - - beforeEach(function() - receivedParameters = nil - stateMachine = StateMachine.new(initialState, eventsByName) - end) - - it(BEFORE_EVENT_SIGNAL, function() - local mainThread = coroutine.running() - local timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, true) - end) - - local signalConnection = stateMachine[BEFORE_EVENT_SIGNAL]:Connect(function(...) - receivedParameters = { ... } - task.cancel(timeoutThread) - coroutine.resume(mainThread, false) - end) - - stateMachine:handle(handledEventName, table.unpack(variadicArgs)) - - local didTimeOut = coroutine.yield(mainThread) - signalConnection:Disconnect() - expect(didTimeOut).toBe(false) - expect(#receivedParameters).toBe(2) - expect(receivedParameters[1]).toBe(handledEventName) - expect(receivedParameters[2]).toBe(initialState) - expect(stateMachine._currentState).toBe(initialState) - end) - - it(LEAVING_STATE_SIGNAL, function() - local expectedAfterState = Y_STATE - local mainThread = coroutine.running() - local timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, true) - end) - - local signalConnection = stateMachine[LEAVING_STATE_SIGNAL]:Connect(function(...) - receivedParameters = { ... } - task.cancel(timeoutThread) - coroutine.resume(mainThread, false) - end) - - stateMachine:handle(handledEventName, table.unpack(variadicArgs)) - - local didTimeOut = coroutine.yield(mainThread) - signalConnection:Disconnect() - expect(didTimeOut).toBe(false) - expect(#receivedParameters).toBe(2) - expect(receivedParameters[1]).toBe(initialState) - expect(receivedParameters[2]).toBe(expectedAfterState) - expect(stateMachine._currentState).toBe(initialState) - end) - - it(STATE_ENTERED_SIGNAL, function() - local expectedAfterState = Y_STATE - local mainThread = coroutine.running() - local timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, true) - end) - - local signalConnection = stateMachine[STATE_ENTERED_SIGNAL]:Connect(function(...) - receivedParameters = { ... } - task.cancel(timeoutThread) - coroutine.resume(mainThread, false) - end) - - stateMachine:handle(handledEventName, table.unpack(variadicArgs)) - - local didTimeOut = coroutine.yield(mainThread) - signalConnection:Disconnect() - expect(didTimeOut).toBe(false) - expect(#receivedParameters).toBe(2) - expect(receivedParameters[1]).toBe(expectedAfterState) - expect(receivedParameters[2]).toBe(initialState) - expect(stateMachine._currentState).toBe(expectedAfterState) - end) - - it(AFTER_EVENT_SIGNAL, function() - local expectedAfterState = Y_STATE - local mainThread = coroutine.running() - local timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, true) - end) - - local signalConnection = stateMachine[AFTER_EVENT_SIGNAL]:Connect(function(...) - receivedParameters = { ... } - task.cancel(timeoutThread) - coroutine.resume(mainThread, false) - end) +-- local TO_X_HANDLER = to(X_STATE) +-- local TO_Y_HANDLER = to(Y_STATE) +-- local FINISH_HANDLER = to(FINISH_STATE) - stateMachine:handle(handledEventName, table.unpack(variadicArgs)) - - local didTimeOut = coroutine.yield(mainThread) - signalConnection:Disconnect() - expect(didTimeOut).toBe(false) - expect(#receivedParameters).toBe(3) - expect(receivedParameters[1]).toBe(handledEventName) - expect(receivedParameters[2]).toBe(expectedAfterState) - expect(receivedParameters[3]).toBe(initialState) - expect(stateMachine._currentState).toBe(expectedAfterState) - end) - - it(FINISHED_SIGNAL, function() - local mainThread = coroutine.running() - local timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, true) - end) - - local signalConnection = stateMachine[FINISHED_SIGNAL]:Connect(function(...) - receivedParameters = { ... } - task.cancel(timeoutThread) - coroutine.resume(mainThread, false) - end) - - stateMachine:handle(FINISH_EVENT, table.unpack(variadicArgs)) - - local didTimeOut = coroutine.yield(mainThread) - signalConnection:Disconnect() - expect(didTimeOut).toBe(false) - expect(#receivedParameters).toBe(1) - expect(receivedParameters[1]).toBe(initialState) - expect(stateMachine._currentState).toBe(FINISH_STATE) - end) - end) - end) - - describe("should invoke callbacks", function() - it("at the correct time", function() - local initialState = X_STATE - local mainThread = coroutine.running() - local timeoutThreadBefore, timeoutThreadAfter - local firedSignals = {} - local eventsByName = { - [FINISH_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = function() - task.cancel(timeoutThreadBefore) - coroutine.resume(mainThread, "beforeAsync") - return FINISH_STATE - end, - afterAsync = function() - task.cancel(timeoutThreadAfter) - coroutine.resume(mainThread, "afterAsync") - end, - }, - }, - }, - } - - local stateMachine = StateMachine.new(initialState, eventsByName) - local signalConnections = {} - - stateMachine:handle(FINISH_EVENT) - - table.insert( - signalConnections, - stateMachine[BEFORE_EVENT_SIGNAL]:Connect(function() - table.insert(firedSignals, BEFORE_EVENT_SIGNAL) - end) - ) - - table.insert( - signalConnections, - stateMachine[LEAVING_STATE_SIGNAL]:Connect(function() - table.insert(firedSignals, LEAVING_STATE_SIGNAL) - end) - ) - - table.insert( - signalConnections, - stateMachine[STATE_ENTERED_SIGNAL]:Connect(function() - table.insert(firedSignals, STATE_ENTERED_SIGNAL) - end) - ) - - table.insert( - signalConnections, - stateMachine[AFTER_EVENT_SIGNAL]:Connect(function() - table.insert(firedSignals, AFTER_EVENT_SIGNAL) - end) - ) - - table.insert( - signalConnections, - stateMachine[FINISHED_SIGNAL]:Connect(function() - table.insert(firedSignals, FINISHED_SIGNAL) - end) - ) - - local timeout = 0.5 - timeoutThreadBefore = task.delay(timeout, function() - for _, signalConnection in signalConnections do - signalConnection:Disconnect() - end - coroutine.resume(mainThread, `timeout waiting {timeout} seconds for beforeAsync invocation`) - end) - local invokedCallback = coroutine.yield() - expect(invokedCallback).toBe("beforeAsync") - local expectedFiredSignals = { BEFORE_EVENT_SIGNAL } - expect(firedSignals[1]).toBe(expectedFiredSignals[1]) - expect(#firedSignals).toBe(#expectedFiredSignals) - - timeoutThreadAfter = task.delay(timeout, function() - coroutine.resume(mainThread, `timeout waiting {timeout} seconds for afterAsync invocation`) - end) - invokedCallback = coroutine.yield() - for _, signalConnection in signalConnections do - signalConnection:Disconnect() - end - expect(invokedCallback).toBe("afterAsync") - expectedFiredSignals = { - BEFORE_EVENT_SIGNAL, - LEAVING_STATE_SIGNAL, - STATE_ENTERED_SIGNAL, - } - for index, expectedFiredSignal in expectedFiredSignals do - expect(firedSignals[index]).toBe(expectedFiredSignal) - end - expect(#firedSignals).toBe(#expectedFiredSignals) - end) - - it("with the correct parameters and state", function() - local initialState = X_STATE - local variadicArgs = { "test", false, nil, 3.5 } - local timeout = 0.5 - local timeoutThread - local mainThread = coroutine.running() - local handledEventName = FINISH_EVENT - -- local expectedAfterState = FINISH_STATE - local receivedParameters - local stateMachine - local eventsByName = { - [FINISH_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = function(...) - receivedParameters = { ... } - task.cancel(timeoutThread) - coroutine.resume(mainThread, "beforeAsync") - return FINISH_STATE - end, - afterAsync = function(...) - receivedParameters = { ... } - task.cancel(timeoutThread) - coroutine.resume(mainThread, "afterAsync") - end, - }, - }, - }, - } - - stateMachine = StateMachine.new(initialState, eventsByName) - - -- Test beforeAsync - timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, `timeout waiting {timeout} seconds for beforeAsync invocation`) - end) - - stateMachine:handle(handledEventName, table.unpack(variadicArgs)) - - local invokedCallback = coroutine.yield(mainThread) - expect(invokedCallback).toBe("beforeAsync") - for index, variadicArg in variadicArgs do - expect(receivedParameters[index]).toBe(variadicArg) - end - expect(#receivedParameters).toBe(#variadicArgs) - expect(stateMachine._currentState).toBe(initialState) - - -- Test afterAsync - timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, `timeout waiting {timeout} seconds for afterAsync invocation`) - end) - - -- local invokedCallback = coroutine.yield(mainThread) - -- expect(invokedCallback).toBe("afterAsync") - -- for index, variadicArg in variadicArgs do - -- expect(receivedParameters[index]).toBe(variadicArg) - -- end - -- expect(#receivedParameters).toBe(#variadicArgs) - -- expect(stateMachine._currentState).toBe(expectedAfterState) - end) - end) - - it("should queue and process async events in FIFO order", function() - local initialState = X_STATE - local timeout = 0.5 - local mainThread = coroutine.running() - local orderedHandledEvents = { - TO_Y_EVENT, - TO_X_EVENT, - FINISH_EVENT, - } - local actualHandledEvents = {} - local eventsByName = { - [TO_Y_EVENT] = { - canBeFinal = false, - from = { - [X_STATE] = { - beforeAsync = TO_Y_HANDLER, - afterAsync = function() - table.insert(actualHandledEvents, TO_Y_EVENT) - task.wait() - end, - }, - }, - }, - [TO_X_EVENT] = { - canBeFinal = false, - from = { - [Y_STATE] = { - beforeAsync = TO_X_HANDLER, - afterAsync = function() - table.insert(actualHandledEvents, TO_X_EVENT) - task.wait() - end, - }, - }, - }, - [FINISH_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = FINISH_HANDLER, - afterAsync = function() - table.insert(actualHandledEvents, FINISH_EVENT) - task.wait() - end, - }, - }, - }, - } - - local stateMachine = StateMachine.new(initialState, eventsByName) - - -- Queue events - for _, handledEventName in ipairs(orderedHandledEvents) do - stateMachine:handle(handledEventName) - end - - local timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, `timeout waiting {timeout} seconds for finished signal`) - end) - - local signalConnection = stateMachine[FINISHED_SIGNAL]:Connect(function() - task.cancel(timeoutThread) - coroutine.resume(mainThread, FINISHED_SIGNAL) - end) - - local invokedCallback = coroutine.yield(mainThread) - signalConnection:Disconnect() - expect(invokedCallback).toBe(FINISHED_SIGNAL) - for index, expectedHandledEventName in orderedHandledEvents do - expect(actualHandledEvents[index]).toBe(expectedHandledEventName) - end - expect(#actualHandledEvents).toBe(#orderedHandledEvents) - end) - - it("should error if called after the machine finished", function() - local eventsByName = { - [FINISH_EVENT] = { - canBeFinal = true, - from = { - [X_STATE] = { - beforeAsync = FINISH_HANDLER, - }, - }, - }, - } - - local stateMachine = StateMachine.new(X_STATE, eventsByName) - local mainThread = coroutine.running() - local logger = stateMachine:getLogger() - - local timeout = 0.5 - local timeoutMessage = `timeout waiting {timeout} seconds for finished or error message` - local timeoutThread = task.delay(timeout, function() - coroutine.resume(mainThread, timeoutMessage) - end) - - stateMachine:handle(FINISH_EVENT) - stateMachine.finished:Wait() - expect(stateMachine._currentState).toBe(FINISH_STATE) - - logger:addHandler(logger.LogLevel.Error, function(level: Logger.LogLevel, _name: string, message: string) - if level ~= logger.LogLevel.Error then - return - end - - if coroutine.status(timeoutThread) ~= "suspended" then - return - end - - task.cancel(timeoutThread) - coroutine.resume(mainThread, message) - return logger.HandlerResult.Sink - end) - - stateMachine:handle(FINISH_EVENT) - local errorMessage = coroutine.yield() - expect(errorMessage == timeoutMessage).never.toBe(true) - expect(errorMessage).toEqual( - expect.stringContaining(`Attempt to process event {FINISH_EVENT} after the state machine already finished`) - ) +describe("test", function() + it("should pass", function() + expect(true).toBe(true) end) end) - --- Placeholder --- describe("getState", function() --- it("should return the current state correctly", function() --- local initialState = "A" --- local eventsByName = { --- toB = { --- canBeFinal = false, --- from = { --- A = { --- beforeAsync = TO_Y_HANDLER, --- }, --- }, --- }, --- } - --- local stateMachine = StateMachine.new(initialState, eventsByName) - --- -- Test getting the current state --- local state = stateMachine:getState() --- expect(state).toBe(initialState) - --- -- Test getting the state after a transition --- stateMachine:handle("toB") --- state = stateMachine:getState() --- expect(state).toBe("B") --- end) --- end) - --- Placeholder --- describe("getValidEvents", function() --- it("should return valid events correctly", function() --- local initialState = "A" --- local eventsByName = { --- toB = { --- canBeFinal = false, --- from = { --- A = { --- beforeAsync = TO_Y_HANDLER, --- }, --- }, --- }, --- toA = { --- canBeFinal = false, --- from = { --- B = { --- beforeAsync = TO_X_HANDLER, --- }, --- }, --- }, --- } - --- local stateMachine = StateMachine.new(initialState, eventsByName) - --- -- Test getting valid events for the initial state --- local validEvents = stateMachine:getValidEvents() --- expect(#validEvents).toBe(1) --- expect(validEvents[1]).toBe("toB") - --- -- Test getting valid events after a transition --- stateMachine:handle("toB") --- validEvents = stateMachine:getValidEvents() --- expect(#validEvents).toBe(1) --- expect(validEvents[1]).toBe("toA") --- end) --- end) - --- Done --- describe("_isDebugEnabled", function() --- it("should default to false", function() --- local eventsByName = { --- [TO_X_EVENT] = { --- canBeFinal = true, --- from = { --- [X_STATE] = { --- beforeAsync = TO_X_HANDLER, --- }, --- }, --- }, --- } - --- local stateMachine = StateMachine.new(X_STATE, eventsByName) --- expect(stateMachine._isDebugEnabled).toBe(false) --- end) - --- describe("setter", function() --- it("should error given a bad type", function() --- local eventsByName = { --- [TO_X_EVENT] = { --- canBeFinal = true, --- from = { --- [X_STATE] = { --- beforeAsync = TO_X_HANDLER, --- }, --- }, --- }, --- } - --- local stateMachine = StateMachine.new(X_STATE, eventsByName) - --- local badTypes = { --- 1, --- "bad", --- nil, --- {}, --- } - --- for _, badType in badTypes do --- expect(function() --- stateMachine:setDebugEnabled(badType) --- end).toThrow(`boolean expected, got {typeof(badType)}`) --- end --- end) - --- it("should set the value correctly", function() --- local eventsByName = { --- [TO_X_EVENT] = { --- canBeFinal = true, --- from = { --- [X_STATE] = { --- beforeAsync = TO_X_HANDLER, --- }, --- }, --- }, --- } - --- local stateMachine = StateMachine.new(X_STATE, eventsByName) - --- stateMachine:setDebugEnabled(true) --- expect(stateMachine._isDebugEnabled).toBe(true) - --- stateMachine:setDebugEnabled(false) --- expect(stateMachine._isDebugEnabled).toBe(false) --- end) --- end) --- end) - --- Placeholder --- describe("destroy", function() --- it("should destroy correctly", function() --- local initialState = "A" --- local eventsByName = { --- toB = { --- canBeFinal = false, --- from = { --- A = { --- beforeAsync = TO_Y_HANDLER, --- }, --- }, --- }, --- } - --- local stateMachine = StateMachine.new(initialState, eventsByName) - --- -- Test destroying the state machine --- stateMachine:destroy() --- expect(stateMachine._isDestroyed).toBe(true) --- expect(stateMachine[BEFORE_EVENT_SIGNAL]:getConnectionCount()).toBe(0) --- expect(stateMachine[LEAVING_STATE_SIGNAL]:getConnectionCount()).toBe(0) --- expect(stateMachine[STATE_ENTERED_SIGNAL]:getConnectionCount()).toBe(0) --- expect(stateMachine[AFTER_EVENT_SIGNAL]:getConnectionCount()).toBe(0) --- expect(stateMachine[FINISHED_SIGNAL]:getConnectionCount()).toBe(0) --- end) --- end) - --- return function() --- local StateMachine = require(script.Parent.Parent.StateMachine) - --- local function createTestMachine() --- local states = { --- ["A"] = { --- ["toB"] = { --- canBeFinal = false, --- from = { --- ["A"] = { --- beforeAsync = function() --- wait(0.1) --- return "B" --- end, --- }, --- }, --- }, --- }, --- ["B"] = { --- ["toC"] = { --- canBeFinal = false, --- from = { --- ["B"] = { --- beforeAsync = function() --- wait(0.1) --- return "C" --- end, --- }, --- }, --- }, --- ["toA"] = { --- canBeFinal = false, --- from = { --- ["B"] = { --- beforeAsync = function() --- wait(0.1) --- return "A" --- end, --- }, --- }, --- }, --- }, --- ["C"] = { --- ["toA"] = { --- canBeFinal = true, --- from = { --- ["C"] = { --- beforeAsync = function() --- wait(0.1) --- return nil --- end, --- }, --- }, --- }, --- }, --- } - --- local machine = StateMachine.new("A", states) --- machine:setDebugEnabled(true) --- return machine --- end - --- describe("new", function() --- it("should create a new StateMachine", function() --- local machine = createTestMachine() --- expect(machine).never.toBeNil() --- end) - --- it("should require an initial state", function() --- expect(function() --- StateMachine.new() --- end).toThrow() --- end) - --- it("should require events", function() --- expect(function() --- StateMachine.new("A") --- end).toThrow() --- end) --- end) - --- describe("handle", function() --- it("should process events", function() --- local machine = createTestMachine() - --- local events = {} --- machine[FINISHED_SIGNAL]:Connect(function(state) --- table.insert(events, state) --- end) - --- machine:handle("toB") --- machine:handle("toC") --- machine:handle("toA") - --- wait(0.4) - --- expect(events).never.toBeNil() --- expect(events[1]).toBe("B") --- end