From ca9ffa0091a90532ea4ffb5c594436c76989fbc8 Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Tue, 10 Dec 2024 12:11:15 -0500 Subject: [PATCH] New Testing Feature (#211) * New tests * Progress * Build command * Test * Python dependencies * Protocol * Key * JSON * Wait for status * Build tests * Output results * Restructure * Check actual output status * Output text * More tests * Script refactor * Key fix * Pass condition * Status * Cleanup * Trove test * Fixes * Fixes * Fixes * Timer tests * Fix tester * Table tests * Table test * Symbol test * Silo test * BeforeEach and AfterEach fix * Signal test * Shake test * Ser test * Option test * Adjust test * Enum list test * Concur test * Component test * Component * WaitFor test --- .github/workflows/ci.yaml | 46 +- TEST.md | 9 - aftman.toml | 2 +- build_tests.py | 2 +- ci/RunTests.luau | 7 + ci/Test.luau | 447 ++++++++++++++++++ ci/unit.server.luau | 19 - dev.project.json | 6 + future.toml | 0 modules/buffer-util/Buffer.test.luau | 25 + modules/buffer-util/wally.toml | 2 +- .../{init.spec.luau => init.test.luau} | 97 ++-- modules/concur/init.spec.luau | 348 -------------- modules/concur/init.test.luau | 364 ++++++++++++++ modules/enum-list/init.spec.luau | 83 ---- modules/enum-list/init.test.luau | 91 ++++ modules/option/init.spec.luau | 330 ------------- modules/option/init.test.luau | 342 ++++++++++++++ modules/ser/init.spec.luau | 46 -- modules/ser/init.test.luau | 50 ++ .../shake/{init.spec.luau => init.test.luau} | 150 +++--- modules/signal/init.spec.luau | 186 -------- modules/signal/init.test.luau | 190 ++++++++ modules/silo/init.spec.luau | 209 -------- modules/silo/init.test.luau | 215 +++++++++ modules/streamable/Streamable.spec.luau | 137 ------ modules/streamable/StreamableUtil.spec.luau | 60 --- modules/symbol/init.spec.luau | 33 -- modules/symbol/init.test.luau | 37 ++ modules/table-util/init.spec.luau | 427 ----------------- modules/table-util/init.test.luau | 439 +++++++++++++++++ modules/timer/init.spec.luau | 69 --- modules/timer/init.test.luau | 73 +++ modules/trove/init.luau | 1 + modules/trove/init.spec.luau | 196 -------- modules/trove/init.test.luau | 203 ++++++++ .../{init.spec.luau => init.test.luau} | 112 ++--- requirements.txt | 1 + run_tests.py | 108 +++++ selene.toml | 2 +- test.project.json | 2 +- test/wally.lock | 7 +- test/wally.toml | 3 - testez.toml | 66 --- 44 files changed, 2836 insertions(+), 2406 deletions(-) delete mode 100644 TEST.md create mode 100644 ci/RunTests.luau create mode 100644 ci/Test.luau delete mode 100644 ci/unit.server.luau delete mode 100644 future.toml create mode 100644 modules/buffer-util/Buffer.test.luau rename modules/component/{init.spec.luau => init.test.luau} (72%) delete mode 100644 modules/concur/init.spec.luau create mode 100644 modules/concur/init.test.luau delete mode 100644 modules/enum-list/init.spec.luau create mode 100644 modules/enum-list/init.test.luau delete mode 100644 modules/option/init.spec.luau create mode 100644 modules/option/init.test.luau delete mode 100644 modules/ser/init.spec.luau create mode 100644 modules/ser/init.test.luau rename modules/shake/{init.spec.luau => init.test.luau} (53%) delete mode 100644 modules/signal/init.spec.luau create mode 100644 modules/signal/init.test.luau delete mode 100644 modules/silo/init.spec.luau create mode 100644 modules/silo/init.test.luau delete mode 100644 modules/streamable/Streamable.spec.luau delete mode 100644 modules/streamable/StreamableUtil.spec.luau delete mode 100644 modules/symbol/init.spec.luau create mode 100644 modules/symbol/init.test.luau delete mode 100644 modules/table-util/init.spec.luau create mode 100644 modules/table-util/init.test.luau delete mode 100644 modules/timer/init.spec.luau create mode 100644 modules/timer/init.test.luau delete mode 100644 modules/trove/init.spec.luau create mode 100644 modules/trove/init.test.luau rename modules/wait-for/{init.spec.luau => init.test.luau} (56%) create mode 100644 run_tests.py delete mode 100644 testez.toml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c1874569..c8ada5a9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,9 +28,51 @@ jobs: name: Styling runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - uses: JohnnyMorganz/stylua-action@v4 with: token: ${{ secrets.GITHUB_TOKEN }} - version: v0.20.0 + version: v2.0.2 args: --check ./modules + + tests: + name: Tests + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Aftman + uses: ok-nick/setup-aftman@v0.4.2 + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Run test builder + run: python build_tests.py + + - name: Build place file + run: rojo build -o test.rbxl test.project.json + + - name: Upload place file + shell: bash + env: + UID: 6900069600 + PID: 110224315334647 + API_KEY: ${{ secrets.RBXCLOUD_API_KEY }} + FILE: test.rbxl + run: rbxcloud experience publish -a "$API_KEY" -u "$UID" -p "$PID" -t published -f "$FILE" + + - name: Run tests + env: + UID: 6900069600 + PID: 110224315334647 + API_KEY: ${{ secrets.RBXCLOUD_API_KEY }} + FILE: test.rbxl + run: python -u run_tests.py "$UID" "$PID" diff --git a/TEST.md b/TEST.md deleted file mode 100644 index df6f72cb..00000000 --- a/TEST.md +++ /dev/null @@ -1,9 +0,0 @@ -# Testing - -Testing requires Python, Wally, Rojo, and Roblox Studio. - -1. Run `python build_tests.py` or `python build_tests.py watch` -2. Run `rojo` against `test.project.json` -3. Sync project into a new Roblox Studio place -4. Run the Roblox Studio place -5. Check output window for test status diff --git a/aftman.toml b/aftman.toml index 8eb0021f..669bc1aa 100644 --- a/aftman.toml +++ b/aftman.toml @@ -1 +1 @@ -tools = { rojo = "rojo-rbx/rojo@7.4.0" , run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" , wally = "UpliftGames/wally@0.3.2" , selene = "Kampfkarren/selene@0.27.1" , stylua = "JohnnyMorganz/StyLua@0.20.0" } +tools = { rojo = "rojo-rbx/rojo@7.4.4" , run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" , wally = "UpliftGames/wally@0.3.2" , selene = "Kampfkarren/selene@0.27.1" , stylua = "JohnnyMorganz/StyLua@2.0.2", rbxcloud = "Sleitnick/rbxcloud@0.14.0" } diff --git a/build_tests.py b/build_tests.py index afdbbae2..77f79e4d 100644 --- a/build_tests.py +++ b/build_tests.py @@ -24,7 +24,7 @@ def update_test_file(test_path, original_src_path): class WatchHandler(PatternMatchingEventHandler): def __init__(self): - PatternMatchingEventHandler.__init__(self, patterns=["*.lua"], ignore_directories=True, case_sensitive=False) + PatternMatchingEventHandler.__init__(self, patterns=["*.luau"], ignore_directories=True, case_sensitive=False) def on_modified(self, event): original_src_path = event.src_path if original_src_path in files_locked: diff --git a/ci/RunTests.luau b/ci/RunTests.luau new file mode 100644 index 00000000..c6ba25fb --- /dev/null +++ b/ci/RunTests.luau @@ -0,0 +1,7 @@ +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +-- Run tests +return Test.run({ ReplicatedStorage.Modules }) diff --git a/ci/Test.luau b/ci/Test.luau new file mode 100644 index 00000000..b62073a5 --- /dev/null +++ b/ci/Test.luau @@ -0,0 +1,447 @@ +export type TestExpect = { + Not: (self: TestExpect) -> TestExpect, + ToBe: (self: TestExpect, value: any) -> (), + ToBeNaN: (self: TestExpect, value: any) -> (), + ToBeNil: (self: TestExpect) -> (), + ToBeOk: (self: TestExpect) -> (), + ToBeTruthy: (self: TestExpect) -> (), + ToBeFalsy: (self: TestExpect) -> (), + ToThrow: (self: TestExpect, err: any?) -> (), + ToHaveLength: (self: TestExpect, length: number) -> (), + ToHaveProperty: (self: TestExpect, property: string) -> (), + ToBeGreaterThan: (self: TestExpect, value: number) -> (), + ToBeGreaterThanOrEqual: (self: TestExpect, value: number) -> (), + ToBeLessThan: (self: TestExpect, value: number) -> (), + ToBeLessThanOrEqual: (self: TestExpect, value: number) -> (), + ToContain: (self: TestExpect, value: any) -> (), + ToBeNear: (self: TestExpect, value: number, epsilon: number) -> (), + ToBeA: (self: TestExpect, typeOf: string) -> (), +} + +local TestExpect = {} +TestExpect.__index = TestExpect + +function TestExpect:ToBe(value: any) + local condition = self.Value == value + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "!=" else "=="} {value}`, 0) + end +end + +function TestExpect:ToBeNil() + local condition = self.Value == nil + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "!=" else "=="} nil`, 0) + end +end + +function TestExpect:ToBeOk() + local condition = self.Value ~= nil + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "is not" else "is"} ok`, 0) + end +end + +function TestExpect:ToBeNaN() + local condition = self.Value ~= self.Value + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "!=" else "=="} NaN`, 0) + end +end + +function TestExpect:ToThrow(err: any?) + local condition: boolean + if err ~= nil then + condition = (not self.Success) and self.Err == err + else + condition = not self.Success + end + + if self.Flip then + condition = not condition + end + + if not condition then + error(`{if self.Flip then "expected to not throw" else "expected to throw"}`, 0) + end +end + +function TestExpect:ToBeTruthy() + local condition = not not self.Value + + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "not truthy" else "truthy"}`, 0) + end +end + +function TestExpect:ToBeFalsy() + local condition = not self.Value + + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "not falsy" else "falsy"}`, 0) + end +end + +function TestExpect:ToHaveLength(length: number) + local t = typeof(self.Value) + local validType = t == "string" or t == "table" + local condition = validType and #self.Value == length + + if self.Flip and validType then + condition = not condition + end + + if not condition then + if validType then + error(`{t} length ({#self.Value}) {if self.Flip then "!=" else "=="} {length}`, 0) + else + error(`invalid type "{t}" (expected table or string)`, 0) + end + end +end + +function TestExpect:ToHaveProperty(property: string) + local t = typeof(self.Value) + local validType = t == "table" + local condition = validType and self.Value[property] ~= nil + + if self.Flip and validType then + condition = not condition + end + + if not condition then + if validType then + error(`table {if self.Flip then "does not contain" else "contains"} property "{property}"`, 0) + else + error(`invalid type "{t}" (expected table)`, 0) + end + end +end + +function TestExpect:ToBeGreaterThan(value: number) + local condition = self.Value > value + + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "<=" else ">"} {value}`, 0) + end +end + +function TestExpect:ToBeGreaterThanOrEqual(value: number) + local condition = self.Value >= value + + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "<" else ">="} {value}`, 0) + end +end + +function TestExpect:ToBeLessThan(value: number) + local condition = self.Value < value + + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then ">=" else "<"} {value}`, 0) + end +end + +function TestExpect:ToBeLessThanOrEqual(value: number) + local condition = self.Value <= value + + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then ">" else "<="} {value}`, 0) + end +end + +function TestExpect:ToContain(value: any) + local t = typeof(self.Value) + local valid = t == "table" or t == "string" + if t == "string" then + valid = typeof(value) == "string" + end + + local condition = false + + if valid then + if t == "table" then + condition = table.find(self.Value, value) ~= nil + else + condition = string.find(self.Value, value) ~= nil + end + end + + if self.Flip and valid then + condition = not condition + end + + if not condition then + if valid then + error(`"{value}" {if self.Flip then "not in" else "in"} {t}`, 0) + else + error(`invalid type "{t}" (expected table or string)`, 0) + end + end +end + +function TestExpect:ToBeA(t: string) + local condition = typeof(self.Value) == t + + if self.Flip then + condition = not condition + end + + if not condition then + error(`"{typeof(self.Value)}" {if self.Flip then "!=" else "=="} "{t}"`, 0) + end +end + +function TestExpect:ToBeNear(value: number, epsilon: number) + local condition = math.abs(self.Value - value) < epsilon + + if self.Flip then + condition = not condition + end + + if not condition then + error(`{self.Value} {if self.Flip then "is not near" else "is near"} {value}`, 0) + end +end + +function TestExpect:Not() + self.Flip = not self.Flip + return self +end + +------------------------------------------------------------------------------------------------------------ + +export type TestContext = { + Test: (self: TestContext, name: string, fn: () -> ()) -> (), + Describe: (self: TestContext, name: string, fn: () -> ()) -> (), + Expect: (self: TestContext, value: any) -> TestExpect, + BeforeAll: (self: TestContext, fn: () -> ()) -> (), + AfterAll: (self: TestContext, fn: () -> ()) -> (), + BeforeEach: (self: TestContext, fn: () -> ()) -> (), + AfterEach: (self: TestContext, fn: () -> ()) -> (), +} + +type TestContextGroup = { + Name: string, + Items: { any }, + AnyFail: boolean, + AfterAllFns: { () -> () }, + BeforeEachFns: { () -> () }, + AfterEachFns: { () -> () }, +} + +local function CreateGroup(name: string): TestContextGroup + return { + Name = name, + Items = {}, + AnyFail = false, + AfterAllFns = {}, + BeforeEachFns = {}, + AfterEachFns = {}, + } +end + +local TestContext = {} +TestContext.__index = TestContext + +function TestContext.new(root: string): TestContext + local testContext = setmetatable({ + TotalTests = 0, + TotalFails = 0, + Group = CreateGroup(root), + }, TestContext) :: any + + testContext.Current = testContext.Group + + return testContext +end + +function TestContext:Test(name: string, fn: () -> ()) + for _, fn in self.Current.BeforeEachFns do + fn() + end + local success, err = pcall(fn) + self.Current.Items[name] = { + Success = success, + Err = err, + } + self.TotalTests += 1 + if not success then + self.TotalFails += 1 + self.Current.AnyFail = true + end + for _, fn in self.Current.AfterEachFns do + fn() + end +end + +function TestContext:Describe(name: string, fn: () -> ()) + local parentGroup = self.Current + local group = CreateGroup(name) + self.Current = group + parentGroup.Items[name] = group + for _, fn in parentGroup.BeforeEachFns do + fn() + end + fn() + self.Current = parentGroup + if group.AnyFail then + parentGroup.AnyFail = true + end + for _, fn in group.AfterAllFns do + fn() + end + for _, fn in parentGroup.AfterEachFns do + fn() + end +end + +function TestContext:Expect(value: any): TestExpect + local resolvedValue = value + local success, err = true, nil + if typeof(value) == "function" then + success, err = pcall(value) + resolvedValue = if success then err else nil + end + return setmetatable({ Value = resolvedValue, Success = success, Err = err, Flip = false }, TestExpect) :: any +end + +function TestContext:BeforeAll(fn: () -> ()) + fn() +end + +function TestContext:AfterAll(fn: () -> ()) + table.insert(self.Current.AfterAllFns, fn) +end + +function TestContext:BeforeEach(fn: () -> ()) + table.insert(self.Current.BeforeEachFns, fn) +end + +function TestContext:AfterEach(fn: () -> ()) + table.insert(self.Current.AfterEachFns, fn) +end + +------------------------------------------------------------------------------------------------------------ + +local Test = {} + +function Test.run(ancestors: { Instance }) + local tests: { [string]: () -> () } = {} + + for _, ancestor in ancestors do + for _, child in ancestor:GetDescendants() do + if child:IsA("ModuleScript") and string.match(child.Name, "%.test$") ~= nil then + local name = (string.match(child.Name, "(.+)%.test$")) or child.Name + if name == "init" then + name = child.Parent.Name + end + local fn = require(child) + tests[name] = fn + end + end + end + + local results: { TestContext } = {} + local allPass = true + + local totalTests = 0 + local totalFails = 0 + local totalSuccesses = 0 + + for name, test in tests do + local context = TestContext.new(name) + local success, err = pcall(test, context) + if not success then + error(`Test runner failed: {err}`) + end + table.insert(results, context) + if context.Group.AnyFail then + allPass = false + end + context.Current = nil + totalTests += context.TotalTests + totalFails += context.TotalFails + end + totalSuccesses = totalTests - totalFails + + -- Build results: + local output = { "Test Results" } + local function out(str: string) + table.insert(output, str) + end + + out("---------") + out(`{totalTests} Total`) + out(`{totalSuccesses} Passed`) + out(`{totalFails} Failed`) + out("---------") + + local tab = " " + for _, res in results do + local function Output(group, lvl) + out(`{string.rep(tab, lvl)}[{if group.AnyFail then "x" else "✓"}] {group.Name}`) + for name, item in group.Items do + if item.Items then + Output(item, lvl + 1) + else + out( + `{string.rep(tab, lvl + 1)}[{if not item.Success then "x" else "✓"}] {name}{if not item.Success + then `\n{string.rep(tab, lvl + 2)}failed assertion: {item.Err}` + else ""}` + ) + end + end + end + Output(res.Group, 0) + end + + local outputTxt = table.concat(output, "\n") + + return { + AllPass = allPass, + Output = outputTxt, + } +end + +return Test diff --git a/ci/unit.server.luau b/ci/unit.server.luau deleted file mode 100644 index fe8199fd..00000000 --- a/ci/unit.server.luau +++ /dev/null @@ -1,19 +0,0 @@ -print("Running unit tests...") - -local ReplicatedStorage = game:GetService("ReplicatedStorage") -local TestEZ = require(ReplicatedStorage.Test.Packages.TestEZ) - --- Clear out package test files -for _, testFolder in ipairs(ReplicatedStorage.Test.modules:GetChildren()) do - local index = testFolder:FindFirstChild("_Index") - if index then - for _, item in ipairs(index:GetDescendants()) do - if item.Name:match("%.spec$") and item:IsA("ModuleScript") then - item:Destroy() - end - end - end -end - --- Run tests -TestEZ.TestBootstrap:run({ ReplicatedStorage.Test.modules }) diff --git a/dev.project.json b/dev.project.json index 0d180657..de828b20 100644 --- a/dev.project.json +++ b/dev.project.json @@ -7,6 +7,12 @@ "Modules": { "$path": "modules" } + }, + "ServerScriptService": { + "$className": "ServerScriptService", + "TestRunner": { + "$path": "ci" + } } } } diff --git a/future.toml b/future.toml deleted file mode 100644 index e69de29b..00000000 diff --git a/modules/buffer-util/Buffer.test.luau b/modules/buffer-util/Buffer.test.luau new file mode 100644 index 00000000..58b5d349 --- /dev/null +++ b/modules/buffer-util/Buffer.test.luau @@ -0,0 +1,25 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +-- local BufferReader = require(script.Parent.BufferReader) +-- local BufferWriter = require(script.Parent.BufferWriter) + +return function(ctx: Test.TestContext) + ctx:Describe("Some test", function() + ctx:Test("1 + 1 == 2", function() + ctx:Expect(1 + 1):ToBe(2) + end) + ctx:Test("1 + 1 != 3", function() + ctx:Expect(1 + 1):Not():ToBe(3) + end) + ctx:Describe("Nested group", function() + ctx:Test("true == true", function() + ctx:Expect(true):ToBe(true) + ctx:Expect(function() + return 32 + end):ToBe(32) + end) + end) + end) +end diff --git a/modules/buffer-util/wally.toml b/modules/buffer-util/wally.toml index 853dd377..0320c0d3 100644 --- a/modules/buffer-util/wally.toml +++ b/modules/buffer-util/wally.toml @@ -6,4 +6,4 @@ license = "MIT" authors = ["Stephen Leitnick"] registry = "https://github.com/UpliftGames/wally-index" realm = "shared" -exclude = ["node_modules", "package.json", "**/*.ts"] +exclude = ["node_modules", "package.json", "**/*.ts", "**/*.test.luau"] diff --git a/modules/component/init.spec.luau b/modules/component/init.test.luau similarity index 72% rename from modules/component/init.spec.luau rename to modules/component/init.test.luau index 97c4a091..18399c96 100644 --- a/modules/component/init.spec.luau +++ b/modules/component/init.test.luau @@ -1,8 +1,11 @@ -return function() - local Component = require(script.Parent) +local CollectionService = game:GetService("CollectionService") +local RunService = game:GetService("RunService") +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) - local CollectionService = game:GetService("CollectionService") - local RunService = game:GetService("RunService") +return function(ctx: Test.TestContext) + local Component = require(script.Parent) local TAG = "__KnitTestComponent__" @@ -79,24 +82,24 @@ return function() self.DidRenderStepped = true end - beforeAll(function() + ctx:BeforeAll(function() taggedInstanceFolder = Instance.new("Folder") taggedInstanceFolder.Name = "KnitComponentTest" taggedInstanceFolder.Archivable = false taggedInstanceFolder.Parent = workspace end) - afterEach(function() - taggedInstanceFolder:ClearAllChildren() - end) - - afterAll(function() + ctx:AfterAll(function() taggedInstanceFolder:Destroy() TestComponentMain:Destroy() end) - describe("Component", function() - it("should capture start and stop events", function() + ctx:Describe("Component", function() + ctx:AfterEach(function() + taggedInstanceFolder:ClearAllChildren() + end) + + ctx:Test("should capture start and stop events", function() local didStart = 0 local didStop = 0 local started = TestComponentMain.Started:Connect(function() @@ -111,18 +114,18 @@ return function() task.wait() started:Disconnect() stopped:Disconnect() - expect(didStart).to.equal(1) - expect(didStop).to.equal(1) + ctx:Expect(didStart):ToBe(1) + ctx:Expect(didStop):ToBe(1) end) - it("should be able to get component from the instance", function() + ctx:Test("should be able to get component from the instance", function() local instance = CreateTaggedInstance() task.wait() local component = TestComponentMain:FromInstance(instance) - expect(component).to.be.ok() + ctx:Expect(component):ToBeOk() end) - it("should be able to get all component instances existing", function() + ctx:Test("should be able to get all component instances existing", function() local numComponents = 3 local instances = table.create(numComponents) for i = 1, numComponents do @@ -131,37 +134,37 @@ return function() end task.wait() local components = TestComponentMain:GetAll() - expect(components).to.be.a("table") - expect(#components).to.equal(numComponents) - for _, c in ipairs(components) do - expect(table.find(instances, c.Instance)).to.be.ok() + ctx:Expect(components):ToBeA("table") + ctx:Expect(components):ToHaveLength(numComponents) + for _, c in components do + ctx:Expect(table.find(instances, c.Instance)):ToBeOk() end end) - it("should call lifecycle methods and extension functions", function() + ctx:Test("should call lifecycle methods and extension functions", function() local instance = CreateTaggedInstance() task.wait(0.2) local component = TestComponentMain:FromInstance(instance) - expect(component).to.be.ok() - expect(component.Data).to.equal("abcdef") - expect(component.DidHeartbeat).to.equal(true) - expect(component.DidStepped).to.equal(RunService:IsRunning()) - expect(component.DidRenderStepped).to.never.equal(true) + ctx:Expect(component):ToBeOk() + ctx:Expect(component.Data):ToBe("abcdef") + ctx:Expect(component.DidHeartbeat):ToBe(true) + ctx:Expect(component.DidStepped):ToBe(RunService:IsRunning()) + ctx:Expect(component.DidRenderStepped):Not():ToBe(true) instance:Destroy() task.wait() - expect(component.Data).to.equal("abcdefghi") + ctx:Expect(component.Data):ToBe("abcdefghi") end) - it("should get another component linked to the same instance", function() + ctx:Test("should get another component linked to the same instance", function() local instance = CreateTaggedInstance() task.wait() local component = TestComponentMain:FromInstance(instance) - expect(component).to.be.ok() - expect(component.Another).to.be.ok() - expect(component.Another:GetData()).to.equal(true) + ctx:Expect(component):ToBeOk() + ctx:Expect(component.Another):ToBeOk() + ctx:Expect(component.Another:GetData()):ToBe(true) end) - it("should use extension to decide whether or not to construct", function() + ctx:Test("should use extension to decide whether or not to construct", function() local e1 = { c = true } function e1.ShouldConstruct(_component) return e1.c @@ -190,9 +193,9 @@ return function() local function Check(inst, comp, shouldExist) local c = comp:FromInstance(inst) if shouldExist then - expect(c).to.be.ok() + ctx:Expect(c):ToBeOk() else - expect(c).to.never.be.ok() + ctx:Expect(c):ToBeNil() end end @@ -221,7 +224,7 @@ return function() CreateAndCheckAll(false, false, false) end) - it("should decide whether or not to use extend", function() + ctx:Test("should decide whether or not to use extend", function() local e1 = { extend = true } function e1.ShouldExtend(_component) return e1.extend @@ -246,16 +249,16 @@ return function() local instance = CreateTaggedInstance() task.wait() local component = TestComponent:FromInstance(instance) - expect(component).to.be.ok() + ctx:Expect(component):ToBeOk() if ex1 then - expect(component.E1).to.equal(true) + ctx:Expect(component.E1):ToBe(true) else - expect(component.E1).to.never.be.ok() + ctx:Expect(component.E1):ToBeNil() end if ex2 then - expect(component.E2).to.equal(true) + ctx:Expect(component.E2):ToBe(true) else - expect(component.E2).to.never.be.ok() + ctx:Expect(component.E2):ToBeNil() end end @@ -265,7 +268,7 @@ return function() SetAndCheck(false, true) end) - it("should allow yielding within construct", function() + ctx:Test("should allow yielding within construct", function() local CUSTOM_TAG = "CustomTag" local TestComponent = Component.new({ Tag = CUSTOM_TAG }) @@ -286,12 +289,12 @@ return function() task.wait(0.6) - expect(numConstruct).to.equal(1) + ctx:Expect(numConstruct):ToBe(1) p:Destroy() newP:Destroy() end) - it("should wait for instance", function() + ctx:Test("should wait for instance", function() local p = Instance.new("Part") p.Anchored = true p.Parent = workspace @@ -299,9 +302,9 @@ return function() CollectionService:AddTag(p, TAG) end) local success, c = TestComponentMain:WaitForInstance(p):timeout(1):await() - expect(success).to.equal(true) - expect(c).to.be.a("table") - expect(c.Instance).to.equal(p) + ctx:Expect(success):ToBe(true) + ctx:Expect(c):ToBeA("table") + ctx:Expect(c.Instance):ToBe(p) p:Destroy() end) end) diff --git a/modules/concur/init.spec.luau b/modules/concur/init.spec.luau deleted file mode 100644 index 7b863dc7..00000000 --- a/modules/concur/init.spec.luau +++ /dev/null @@ -1,348 +0,0 @@ -return function() - local Concur = require(script.Parent) - - local function Awaiter(timeout: number) - local awaiter = {} - local thread - local delayThread - function awaiter.Resume(...) - if coroutine.running() ~= delayThread then - task.cancel(delayThread) - end - task.spawn(thread, ...) - end - function awaiter.Yield() - thread = coroutine.running() - delayThread = task.delay(timeout, function() - awaiter.Resume() - end) - return coroutine.yield() - end - return awaiter - end - - local bindableEvent - beforeEach(function() - bindableEvent = Instance.new("BindableEvent") - end) - afterEach(function() - bindableEvent:Destroy() - bindableEvent = nil - end) - - describe("Single", function() - it("should spawn a new concur instance", function() - local value = nil - expect(function() - Concur.spawn(function() - value = 10 - end) - end).to.never.throw() - expect(value).to.equal(10) - end) - - it("should defer a new concur instance", function() - local awaiter = Awaiter(1) - expect(function() - Concur.defer(function() - awaiter.Resume(10) - end) - end).to.never.throw() - local value = awaiter.Yield() - expect(value).to.equal(10) - end) - - it("should delay a new concur instance", function() - local awaiter = Awaiter(1) - expect(function() - Concur.delay(0.1, function() - awaiter.Resume(10) - end) - end).to.never.throw() - local value = awaiter.Yield() - expect(value).to.equal(10) - end) - - it("should create an immediate value concur instance", function() - local c - expect(function() - c = Concur.value(10) - end).to.never.throw() - expect(c).to.be.ok() - expect(c:IsCompleted()).to.equal(true) - local err, val = c:Await() - expect(err).to.never.be.ok() - expect(val).to.equal(10) - end) - - it("should create a concur instance to watch an event with no predicate", function() - local c - expect(function() - c = Concur.event(bindableEvent.Event) - end).to.never.throw() - expect(c:IsCompleted()).to.equal(false) - bindableEvent:Fire(10) - local err, val = c:Await(1) - expect(err).to.never.be.ok() - expect(val).to.equal(10) - end) - - it("should create a concur instance to watch an event with a predicate", function() - local c - expect(function() - c = Concur.event(bindableEvent.Event, function(v) - return v < 10 - end) - end).to.never.throw() - expect(c:IsCompleted()).to.equal(false) - bindableEvent:Fire(10) - bindableEvent:Fire(5) - local err, val = c:Await(1) - expect(err).to.never.be.ok() - expect(val).to.equal(5) - end) - end) - - describe("Multi", function() - it("should complete all concur instances", function() - local c1 = Concur.spawn(function() - return 10 - end) - local c2 = Concur.defer(function() - return 20 - end) - local c3 = Concur.delay(0, function() - return 30 - end) - local c4 = Concur.spawn(function() - error("fail") - end) - local c5 = Concur.event(bindableEvent.Event) - local c = Concur.all({ c1, c2, c3, c4, c5 }) - expect(c:IsCompleted()).to.equal(false) - bindableEvent:Fire(40) - local err, res = c:Await(1) - expect(err).to.never.be.ok() - expect(res[1][1]).to.never.be.ok() - expect(res[1][2]).to.equal(10) - expect(res[2][1]).to.never.be.ok() - expect(res[2][2]).to.equal(20) - expect(res[3][1]).to.never.be.ok() - expect(res[3][2]).to.equal(30) - expect(res[4][1]).to.be.ok() - expect(res[4][2]).to.never.be.ok() - expect(res[5][1]).to.never.be.ok() - expect(res[5][2]).to.equal(40) - end) - - it("should complete the first concur instance", function() - local c1 = Concur.defer(function() - return 10 - end) - local c2 = Concur.spawn(function() - return 20 - end) - local c = Concur.first({ c1, c2 }) - local err, res = c:Await(1) - expect(err).to.never.be.ok() - expect(res).to.equal(20) - end) - end) - - describe("Stop", function() - it("should stop a single concur", function() - local c1 = Concur.defer(function() - return 10 - end) - expect(c1:IsCompleted()).to.equal(false) - c1:Stop() - expect(c1:IsCompleted()).to.equal(true) - local err, val = c1:Await() - expect(err).to.equal(Concur.Errors.Stopped) - expect(val).to.never.be.ok() - end) - - it("should stop multiple concurs", function() - local c1 = Concur.defer(function() end) - local c2 = Concur.delay(1, function() end) - local c3 = Concur.event(bindableEvent.Event) - local c = Concur.all({ c1, c2, c3 }) - c:Stop() - local err, val = c:Await() - expect(err).to.equal(Concur.Errors.Stopped) - expect(val).to.never.be.ok() - end) - - it("should not stop an already completed concur", function() - local c1 = Concur.spawn(function() - return 10 - end) - expect(c1:IsCompleted()).to.equal(true) - c1:Stop() - local err, val = c1:Await() - expect(err).to.never.be.ok() - expect(val).to.equal(10) - end) - end) - - describe("IsCompleted", function() - it("should correctly check if a concur instance is completed", function() - local c1 = Concur.defer(function() end) - expect(c1:IsCompleted()).to.equal(false) - local err = c1:Await() - expect(err).to.never.be.ok() - expect(c1:IsCompleted()).to.equal(true) - end) - - it("should be marked as completed if error", function() - local c1 = Concur.spawn(function() - error("err") - end) - expect(c1:IsCompleted()).to.equal(true) - end) - - it("should be marked as completed if stopped", function() - local c1 = Concur.defer(function() end) - c1:Stop() - expect(c1:IsCompleted()).to.equal(true) - end) - end) - - describe("Await", function() - it("should await concur to be completed", function() - local c1 = Concur.defer(function() - return 10 - end) - local err, val = c1:Await(1) - expect(err).to.never.be.ok() - expect(val).to.equal(10) - end) - - it("should await concur to be completed even if error", function() - local c1 = Concur.defer(function() - return error("err") - end) - local err, val = c1:Await(1) - expect(err).to.be.ok() - expect(val).to.never.be.ok() - end) - - it("should await concur to be completed even if stopped", function() - local c1 = Concur.delay(0.1, function() - return 10 - end) - task.defer(function() - c1:Stop() - end) - local err, val = c1:Await(1) - expect(err).to.equal(Concur.Errors.Stopped) - expect(val).to.never.be.ok() - end) - - it("should return completed values immediately if already completed", function() - local c1 = Concur.spawn(function() - return 10 - end) - expect(c1:IsCompleted()).to.equal(true) - local err, val = c1:Await() - expect(err).to.never.be.ok() - expect(val).to.equal(10) - end) - - it("should timeout", function() - local c1 = Concur.delay(0.2, function() - return 10 - end) - local err, val = c1:Await(0.1) - expect(err).to.equal(Concur.Errors.Timeout) - expect(val).to.never.be.ok() - err, val = c1:Await() - expect(err).to.never.be.ok() - expect(val).to.equal(10) - end) - end) - - describe("OnCompleted", function() - it("should fire function once completed", function() - local awaiter = Awaiter(0.1) - local c1 = Concur.defer(function() - return 10 - end) - expect(c1:IsCompleted()).to.equal(false) - c1:OnCompleted(function(err, val) - awaiter.Resume(err, val) - end) - local err, val = awaiter.Yield() - expect(err).to.never.be.ok() - expect(val).to.equal(10) - end) - - it("should fire function even if already completed", function() - local c1 = Concur.spawn(function() - return 10 - end) - expect(c1:IsCompleted()).to.equal(true) - local err, val - c1:OnCompleted(function(e, v) - err, val = e, v - end) - expect(err).to.never.be.ok() - expect(val).to.equal(10) - end) - - it("should fire function even if error", function() - local awaiter = Awaiter(0.1) - local c1 = Concur.defer(function() - error("err") - end) - c1:OnCompleted(function(err, val) - awaiter.Resume(err, val) - end) - local err, val = awaiter.Yield() - expect(err).to.be.ok() - expect(val).to.never.be.ok() - end) - - it("should fire function even if stopped", function() - local awaiter = Awaiter(0.2) - local c1 = Concur.delay(0.1, function() - error("err") - end) - c1:OnCompleted(function(err, val) - awaiter.Resume(err, val) - end) - task.defer(function() - c1:Stop() - end) - local err, val = awaiter.Yield() - expect(err).to.equal(Concur.Errors.Stopped) - expect(val).to.never.be.ok() - end) - - it("should fire function even if timeout", function() - local awaiter = Awaiter(0.5) - local c1 = Concur.delay(0.2, function() - error("err") - end) - c1:OnCompleted(function(err, val) - awaiter.Resume(err, val) - end, 0.1) - local err, val = awaiter.Yield() - expect(err).to.equal(Concur.Errors.Timeout) - expect(val).to.never.be.ok() - end) - - it("should unbind function", function() - local c1 = Concur.defer(function() end) - local val = nil - local unbind = c1:OnCompleted(function() - val = 10 - end) - unbind() - local err = c1:Await() - expect(err).to.never.be.ok() - task.wait() - expect(val).to.never.be.ok() - end) - end) -end diff --git a/modules/concur/init.test.luau b/modules/concur/init.test.luau new file mode 100644 index 00000000..4a5fecea --- /dev/null +++ b/modules/concur/init.test.luau @@ -0,0 +1,364 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local Concur = require(script.Parent) + + local function Awaiter(timeout: number) + local awaiter = {} + local thread + local delayThread + function awaiter.Resume(...) + if coroutine.running() ~= delayThread then + task.cancel(delayThread) + end + task.spawn(thread, ...) + end + function awaiter.Yield() + thread = coroutine.running() + delayThread = task.delay(timeout, function() + awaiter.Resume() + end) + return coroutine.yield() + end + return awaiter + end + + local bindableEvent + ctx:BeforeEach(function() + bindableEvent = Instance.new("BindableEvent") + end) + ctx:AfterEach(function() + bindableEvent:Destroy() + bindableEvent = nil + end) + + ctx:Describe("Single", function() + ctx:Test("should spawn a new concur instance", function() + local value = nil + ctx:Expect(function() + Concur.spawn(function() + value = 10 + end) + end) + :Not() + :ToThrow() + ctx:Expect(value):ToBe(10) + end) + + ctx:Test("should defer a new concur instance", function() + local awaiter = Awaiter(1) + ctx:Expect(function() + Concur.defer(function() + awaiter.Resume(10) + end) + end) + :Not() + :ToThrow() + local value = awaiter.Yield() + ctx:Expect(value):ToBe(10) + end) + + ctx:Test("should delay a new concur instance", function() + local awaiter = Awaiter(1) + ctx:Expect(function() + Concur.delay(0.1, function() + awaiter.Resume(10) + end) + end) + :Not() + :ToThrow() + local value = awaiter.Yield() + ctx:Expect(value):ToBe(10) + end) + + ctx:Test("should create an immediate value concur instance", function() + local c + ctx:Expect(function() + c = Concur.value(10) + end) + :Not() + :ToThrow() + ctx:Expect(c):ToBeOk() + ctx:Expect(c:IsCompleted()):ToBe(true) + local err, val = c:Await() + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(10) + end) + + ctx:Test("should create a concur instance to watch an event with no predicate", function() + local c + ctx:Expect(function() + c = Concur.event(bindableEvent.Event) + end) + :Not() + :ToThrow() + ctx:Expect(c:IsCompleted()):ToBe(false) + bindableEvent:Fire(10) + local err, val = c:Await(1) + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(10) + end) + + ctx:Test("should create a concur instance to watch an event with a predicate", function() + local c + ctx:Expect(function() + c = Concur.event(bindableEvent.Event, function(v) + return v < 10 + end) + end) + :Not() + :ToThrow() + ctx:Expect(c:IsCompleted()):ToBe(false) + bindableEvent:Fire(10) + bindableEvent:Fire(5) + local err, val = c:Await(1) + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(5) + end) + end) + + ctx:Describe("Multi", function() + ctx:Test("should complete all concur instances", function() + local c1 = Concur.spawn(function() + return 10 + end) + local c2 = Concur.defer(function() + return 20 + end) + local c3 = Concur.delay(0, function() + return 30 + end) + local c4 = Concur.spawn(function() + error("fail") + end) + local c5 = Concur.event(bindableEvent.Event) + local c = Concur.all({ c1, c2, c3, c4, c5 }) + ctx:Expect(c:IsCompleted()):ToBe(false) + bindableEvent:Fire(40) + local err, res = c:Await(1) + ctx:Expect(err):ToBeNil() + ctx:Expect(res[1][1]):ToBeNil() + ctx:Expect(res[1][2]):ToBe(10) + ctx:Expect(res[2][1]):ToBeNil() + ctx:Expect(res[2][2]):ToBe(20) + ctx:Expect(res[3][1]):ToBeNil() + ctx:Expect(res[3][2]):ToBe(30) + ctx:Expect(res[4][1]):ToBeOk() + ctx:Expect(res[4][2]):ToBeNil() + ctx:Expect(res[5][1]):ToBeNil() + ctx:Expect(res[5][2]):ToBe(40) + end) + + ctx:Test("should complete the first concur instance", function() + local c1 = Concur.defer(function() + return 10 + end) + local c2 = Concur.spawn(function() + return 20 + end) + local c = Concur.first({ c1, c2 }) + local err, res = c:Await(1) + ctx:Expect(err):ToBeNil() + ctx:Expect(res):ToBe(20) + end) + end) + + ctx:Describe("Stop", function() + ctx:Test("should stop a single concur", function() + local c1 = Concur.defer(function() + return 10 + end) + ctx:Expect(c1:IsCompleted()):ToBe(false) + c1:Stop() + ctx:Expect(c1:IsCompleted()):ToBe(true) + local err, val = c1:Await() + ctx:Expect(err):ToBe(Concur.Errors.Stopped) + ctx:Expect(val):ToBeNil() + end) + + ctx:Test("should stop multiple concurs", function() + local c1 = Concur.defer(function() end) + local c2 = Concur.delay(1, function() end) + local c3 = Concur.event(bindableEvent.Event) + local c = Concur.all({ c1, c2, c3 }) + c:Stop() + local err, val = c:Await() + ctx:Expect(err):ToBe(Concur.Errors.Stopped) + ctx:Expect(val):ToBeNil() + end) + + ctx:Test("should not stop an already completed concur", function() + local c1 = Concur.spawn(function() + return 10 + end) + ctx:Expect(c1:IsCompleted()):ToBe(true) + c1:Stop() + local err, val = c1:Await() + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(10) + end) + end) + + ctx:Describe("IsCompleted", function() + ctx:Test("should correctly check if a concur instance is completed", function() + local c1 = Concur.defer(function() end) + ctx:Expect(c1:IsCompleted()):ToBe(false) + local err = c1:Await() + ctx:Expect(err):ToBeNil() + ctx:Expect(c1:IsCompleted()):ToBe(true) + end) + + ctx:Test("should be marked as completed if error", function() + local c1 = Concur.spawn(function() + error("err") + end) + ctx:Expect(c1:IsCompleted()):ToBe(true) + end) + + ctx:Test("should be marked as completed if stopped", function() + local c1 = Concur.defer(function() end) + c1:Stop() + ctx:Expect(c1:IsCompleted()):ToBe(true) + end) + end) + + ctx:Describe("Await", function() + ctx:Test("should await concur to be completed", function() + local c1 = Concur.defer(function() + return 10 + end) + local err, val = c1:Await(1) + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(10) + end) + + ctx:Test("should await concur to be completed even if error", function() + local c1 = Concur.defer(function() + return error("err") + end) + local err, val = c1:Await(1) + ctx:Expect(err):ToBeOk() + ctx:Expect(val):ToBeNil() + end) + + ctx:Test("should await concur to be completed even if stopped", function() + local c1 = Concur.delay(0.1, function() + return 10 + end) + task.defer(function() + c1:Stop() + end) + local err, val = c1:Await(1) + ctx:Expect(err):ToBe(Concur.Errors.Stopped) + ctx:Expect(val):ToBeNil() + end) + + ctx:Test("should return completed values immediately if already completed", function() + local c1 = Concur.spawn(function() + return 10 + end) + ctx:Expect(c1:IsCompleted()):ToBe(true) + local err, val = c1:Await() + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(10) + end) + + ctx:Test("should timeout", function() + local c1 = Concur.delay(0.2, function() + return 10 + end) + local err, val = c1:Await(0.1) + ctx:Expect(err):ToBe(Concur.Errors.Timeout) + ctx:Expect(val):ToBeNil() + err, val = c1:Await() + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(10) + end) + end) + + ctx:Describe("OnCompleted", function() + ctx:Test("should fire function once completed", function() + local awaiter = Awaiter(0.1) + local c1 = Concur.defer(function() + return 10 + end) + ctx:Expect(c1:IsCompleted()):ToBe(false) + c1:OnCompleted(function(err, val) + awaiter.Resume(err, val) + end) + local err, val = awaiter.Yield() + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(10) + end) + + ctx:Test("should fire function even if already completed", function() + local c1 = Concur.spawn(function() + return 10 + end) + ctx:Expect(c1:IsCompleted()):ToBe(true) + local err, val + c1:OnCompleted(function(e, v) + err, val = e, v + end) + ctx:Expect(err):ToBeNil() + ctx:Expect(val):ToBe(10) + end) + + ctx:Test("should fire function even if error", function() + local awaiter = Awaiter(0.1) + local c1 = Concur.defer(function() + error("err") + end) + c1:OnCompleted(function(err, val) + awaiter.Resume(err, val) + end) + local err, val = awaiter.Yield() + ctx:Expect(err):ToBeOk() + ctx:Expect(val):ToBeNil() + end) + + ctx:Test("should fire function even if stopped", function() + local awaiter = Awaiter(0.2) + local c1 = Concur.delay(0.1, function() + error("err") + end) + c1:OnCompleted(function(err, val) + awaiter.Resume(err, val) + end) + task.defer(function() + c1:Stop() + end) + local err, val = awaiter.Yield() + ctx:Expect(err):ToBe(Concur.Errors.Stopped) + ctx:Expect(val):ToBeNil() + end) + + ctx:Test("should fire function even if timeout", function() + local awaiter = Awaiter(0.5) + local c1 = Concur.delay(0.2, function() + error("err") + end) + c1:OnCompleted(function(err, val) + awaiter.Resume(err, val) + end, 0.1) + local err, val = awaiter.Yield() + ctx:Expect(err):ToBe(Concur.Errors.Timeout) + ctx:Expect(val):ToBeNil() + end) + + ctx:Test("should unbind function", function() + local c1 = Concur.defer(function() end) + local val = nil + local unbind = c1:OnCompleted(function() + val = 10 + end) + unbind() + local err = c1:Await() + ctx:Expect(err):ToBeNil() + task.wait() + ctx:Expect(val):ToBeNil() + end) + end) +end diff --git a/modules/enum-list/init.spec.luau b/modules/enum-list/init.spec.luau deleted file mode 100644 index 539acf35..00000000 --- a/modules/enum-list/init.spec.luau +++ /dev/null @@ -1,83 +0,0 @@ -return function() - local EnumList = require(script.Parent) - - describe("Constructor", function() - it("should create a new enumlist", function() - expect(function() - EnumList.new("Test", { "ABC", "XYZ" }) - end).never.to.throw() - end) - - it("should fail to create a new enumlist with no name", function() - expect(function() - EnumList.new(nil, { "ABC", "XYZ" }) - end).to.throw() - end) - - it("should fail to create a new enumlist with no enums", function() - expect(function() - EnumList.new("Test") - end).to.throw() - end) - - it("should fail to create a new enumlist with non string enums", function() - expect(function() - EnumList.new("Test", { true, false, 32, "ABC" }) - end).to.throw() - end) - end) - - describe("Access", function() - it("should be able to access enum items", function() - local test = EnumList.new("Test", { "ABC", "XYZ" }) - expect(function() - local _item = test.ABC - end).never.to.throw() - expect(test:BelongsTo(test.ABC)).to.equal(true) - end) - - it("should throw if trying to modify the enumlist", function() - local test = EnumList.new("Test", { "ABC", "XYZ" }) - expect(function() - test.Hello = 32 - end).to.throw() - expect(function() - test.ABC = 32 - end).to.throw() - end) - - it("should throw if trying to modify an enumitem", function() - local test = EnumList.new("Test", { "ABC", "XYZ" }) - expect(function() - local abc = test.ABC - abc.XYZ = 32 - end).to.throw() - expect(function() - local abc = test.ABC - abc.Name = "NewName" - end).to.throw() - end) - - it("should get the name", function() - local test = EnumList.new("Test", { "ABC", "XYZ" }) - local name = test:GetName() - expect(name).to.equal("Test") - end) - end) - - describe("Get Items", function() - it("should be able to get all enum items", function() - local test = EnumList.new("Test", { "ABC", "XYZ" }) - local items = test:GetEnumItems() - expect(items).to.be.a("table") - expect(#items).to.equal(2) - for i, enumItem in ipairs(items) do - expect(enumItem).to.be.a("table") - expect(enumItem.Name).to.be.a("string") - expect(enumItem.Value).to.be.a("number") - expect(enumItem.Value).to.equal(i) - expect(enumItem.EnumType).to.equal(test) - end - end) - end) -end diff --git a/modules/enum-list/init.test.luau b/modules/enum-list/init.test.luau new file mode 100644 index 00000000..5b2084d5 --- /dev/null +++ b/modules/enum-list/init.test.luau @@ -0,0 +1,91 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local EnumList = require(script.Parent) + + ctx:Describe("Constructor", function() + ctx:Test("should create a new enumlist", function() + ctx:Expect(function() + EnumList.new("Test", { "ABC", "XYZ" }) + end) + :Not() + :ToThrow() + end) + + ctx:Test("should fail to create a new enumlist with no name", function() + ctx:Expect(function() + EnumList.new(nil, { "ABC", "XYZ" }) + end):ToThrow() + end) + + ctx:Test("should fail to create a new enumlist with no enums", function() + ctx:Expect(function() + EnumList.new("Test") + end):ToThrow() + end) + + ctx:Test("should fail to create a new enumlist with non string enums", function() + ctx:Expect(function() + EnumList.new("Test", { true, false, 32, "ABC" }) + end):ToThrow() + end) + end) + + ctx:Describe("Access", function() + ctx:Test("should be able to access enum items", function() + local test = EnumList.new("Test", { "ABC", "XYZ" }) + ctx:Expect(function() + local _item = test.ABC + end) + :Not() + :ToThrow() + ctx:Expect(test:BelongsTo(test.ABC)):ToBe(true) + end) + + ctx:Test("should throw if trying to modify the enumlist", function() + local test = EnumList.new("Test", { "ABC", "XYZ" }) + ctx:Expect(function() + test.Hello = 32 + end):ToThrow() + ctx:Expect(function() + test.ABC = 32 + end):ToThrow() + end) + + ctx:Test("should throw if trying to modify an enumitem", function() + local test = EnumList.new("Test", { "ABC", "XYZ" }) + ctx:Expect(function() + local abc = test.ABC + abc.XYZ = 32 + end):ToThrow() + ctx:Expect(function() + local abc = test.ABC + abc.Name = "NewName" + end):ToThrow() + end) + + ctx:Test("should get the name", function() + local test = EnumList.new("Test", { "ABC", "XYZ" }) + local name = test:GetName() + ctx:Expect(name):ToBe("Test") + end) + end) + + ctx:Describe("Get Items", function() + ctx:Test("should be able to get all enum items", function() + local test = EnumList.new("Test", { "ABC", "XYZ" }) + local items = test:GetEnumItems() + ctx:Expect(items):ToBeA("table") + ctx:Expect(#items):ToBe(2) + for i, enumItem in ipairs(items) do + ctx:Expect(enumItem):ToBeA("table") + ctx:Expect(enumItem.Name):ToBeA("string") + ctx:Expect(enumItem.Value):ToBeA("number") + ctx:Expect(enumItem.Value):ToBe(i) + ctx:Expect(enumItem.EnumType):ToBe(test) + end + end) + end) +end diff --git a/modules/option/init.spec.luau b/modules/option/init.spec.luau deleted file mode 100644 index 5bc8fe06..00000000 --- a/modules/option/init.spec.luau +++ /dev/null @@ -1,330 +0,0 @@ -return function() - local Option = require(script.Parent) - - describe("Some", function() - it("should create some option", function() - local opt = Option.Some(true) - expect(opt:IsSome()).to.equal(true) - end) - - it("should fail to create some option with nil", function() - expect(function() - Option.Some(nil) - end).to.throw() - end) - - it("should not be none", function() - local opt = Option.Some(10) - expect(opt:IsNone()).to.equal(false) - end) - end) - - describe("None", function() - it("should be able to reference none", function() - expect(function() - local _none = Option.None - end).never.to.throw() - end) - - it("should be able to check if none", function() - local none = Option.None - expect(none:IsNone()).to.equal(true) - end) - - it("should be able to check if not some", function() - local none = Option.None - expect(none:IsSome()).to.equal(false) - end) - end) - - describe("Equality", function() - it("should equal the same some from same options", function() - local opt = Option.Some(32) - expect(opt).to.equal(opt) - end) - - it("should equal the same some from different options", function() - local opt1 = Option.Some(32) - local opt2 = Option.Some(32) - expect(opt1).to.equal(opt2) - end) - end) - - describe("Assert", function() - it("should assert that a some option is an option", function() - expect(Option.Is(Option.Some(10))).to.equal(true) - end) - - it("should assert that a none option is an option", function() - expect(Option.Is(Option.None)).to.equal(true) - end) - - it("should assert that a non-option is not an option", function() - expect(Option.Is(10)).to.equal(false) - expect(Option.Is(true)).to.equal(false) - expect(Option.Is(false)).to.equal(false) - expect(Option.Is("Test")).to.equal(false) - expect(Option.Is({})).to.equal(false) - expect(Option.Is(function() end)).to.equal(false) - expect(Option.Is(coroutine.create(function() end))).to.equal(false) - expect(Option.Is(Option)).to.equal(false) - end) - end) - - describe("Unwrap", function() - it("should unwrap a some option", function() - local opt = Option.Some(10) - expect(function() - opt:Unwrap() - end).never.to.throw() - expect(opt:Unwrap()).to.equal(10) - end) - - it("should fail to unwrap a none option", function() - local opt = Option.None - expect(function() - opt:Unwrap() - end).to.throw() - end) - end) - - describe("Expect", function() - it("should expect a some option", function() - local opt = Option.Some(10) - expect(function() - opt:Expect("Expecting some value") - end).never.to.throw() - expect(opt:Unwrap()).to.equal(10) - end) - - it("should fail when expecting on a none option", function() - local opt = Option.None - expect(function() - opt:Expect("Expecting some value") - end).to.throw() - end) - end) - - describe("ExpectNone", function() - it("should fail to expect a none option", function() - local opt = Option.Some(10) - expect(function() - opt:ExpectNone("Expecting some value") - end).to.throw() - end) - - it("should expect a none option", function() - local opt = Option.None - expect(function() - opt:ExpectNone("Expecting some value") - end).never.to.throw() - end) - end) - - describe("UnwrapOr", function() - it("should unwrap a some option", function() - local opt = Option.Some(10) - expect(opt:UnwrapOr(20)).to.equal(10) - end) - - it("should unwrap a none option", function() - local opt = Option.None - expect(opt:UnwrapOr(20)).to.equal(20) - end) - end) - - describe("UnwrapOrElse", function() - it("should unwrap a some option", function() - local opt = Option.Some(10) - local result = opt:UnwrapOrElse(function() - return 30 - end) - expect(result).to.equal(10) - end) - - it("should unwrap a none option", function() - local opt = Option.None - local result = opt:UnwrapOrElse(function() - return 30 - end) - expect(result).to.equal(30) - end) - end) - - describe("And", function() - it("should return the second option with and when both are some", function() - local opt1 = Option.Some(1) - local opt2 = Option.Some(2) - expect(opt1:And(opt2)).to.equal(opt2) - end) - - it("should return none when first option is some and second option is none", function() - local opt1 = Option.Some(1) - local opt2 = Option.None - expect(opt1:And(opt2):IsNone()).to.equal(true) - end) - - it("should return none when first option is none and second option is some", function() - local opt1 = Option.None - local opt2 = Option.Some(2) - expect(opt1:And(opt2):IsNone()).to.equal(true) - end) - - it("should return none when both options are none", function() - local opt1 = Option.None - local opt2 = Option.None - expect(opt1:And(opt2):IsNone()).to.equal(true) - end) - end) - - describe("AndThen", function() - it("should pass the some value to the predicate", function() - local opt = Option.Some(32) - opt:AndThen(function(value) - expect(value).to.equal(32) - return Option.None - end) - end) - - it("should throw if an option is not returned from predicate", function() - local opt = Option.Some(32) - expect(function() - opt:AndThen(function() end) - end).to.throw() - end) - - it("should return none if the option is none", function() - local opt = Option.None - expect(opt:AndThen(function() - return Option.Some(10) - end):IsNone()).to.equal(true) - end) - - it("should return option of predicate if option is some", function() - local opt = Option.Some(32) - local result = opt:AndThen(function() - return Option.Some(10) - end) - expect(result:IsSome()).to.equal(true) - expect(result:Unwrap()).to.equal(10) - end) - end) - - describe("Or", function() - it("should return the first option if it is some", function() - local opt1 = Option.Some(10) - local opt2 = Option.Some(20) - expect(opt1:Or(opt2)).to.equal(opt1) - end) - - it("should return the second option if the first one is none", function() - local opt1 = Option.None - local opt2 = Option.Some(20) - expect(opt1:Or(opt2)).to.equal(opt2) - end) - end) - - describe("OrElse", function() - it("should return the first option if it is some", function() - local opt1 = Option.Some(10) - local opt2 = Option.Some(20) - expect(opt1:OrElse(function() - return opt2 - end)).to.equal(opt1) - end) - - it("should return the second option if the first one is none", function() - local opt1 = Option.None - local opt2 = Option.Some(20) - expect(opt1:OrElse(function() - return opt2 - end)).to.equal(opt2) - end) - - it("should throw if the predicate does not return an option", function() - local opt1 = Option.None - expect(function() - opt1:OrElse(function() end) - end).to.throw() - end) - end) - - describe("XOr", function() - it("should return first option if first option is some and second option is none", function() - local opt1 = Option.Some(1) - local opt2 = Option.None - expect(opt1:XOr(opt2)).to.equal(opt1) - end) - - it("should return second option if first option is none and second option is some", function() - local opt1 = Option.None - local opt2 = Option.Some(2) - expect(opt1:XOr(opt2)).to.equal(opt2) - end) - - it("should return none if first and second option are some", function() - local opt1 = Option.Some(1) - local opt2 = Option.Some(2) - expect(opt1:XOr(opt2)).to.equal(Option.None) - end) - - it("should return none if first and second option are none", function() - local opt1 = Option.None - local opt2 = Option.None - expect(opt1:XOr(opt2)).to.equal(Option.None) - end) - end) - - describe("Filter", function() - it("should return none if option is none", function() - local opt = Option.None - expect(opt:Filter(function() end)).to.equal(Option.None) - end) - - it("should return none if option is some but fails predicate", function() - local opt = Option.Some(10) - expect(opt:Filter(function(_v) - return false - end)).to.equal(Option.None) - end) - - it("should return self if option is some and passes predicate", function() - local opt = Option.Some(10) - expect(opt:Filter(function(_v) - return true - end)).to.equal(opt) - end) - end) - - describe("Contains", function() - it("should return true if some option contains the given value", function() - local opt = Option.Some(32) - expect(opt:Contains(32)).to.equal(true) - end) - - it("should return false if some option does not contain the given value", function() - local opt = Option.Some(32) - expect(opt:Contains(64)).to.equal(false) - end) - - it("should return false if option is none", function() - local opt = Option.None - expect(opt:Contains(64)).to.equal(false) - end) - end) - - describe("ToString", function() - it("should return string of none option", function() - local opt = Option.None - expect(tostring(opt)).to.equal("Option") - end) - - it("should return string of some option with type", function() - local values = { 10, true, false, "test", {}, function() end, coroutine.create(function() end), workspace } - for _, value in ipairs(values) do - local expectedString = ("Option<%s>"):format(typeof(value)) - expect(tostring(Option.Some(value))).to.equal(expectedString) - end - end) - end) -end diff --git a/modules/option/init.test.luau b/modules/option/init.test.luau new file mode 100644 index 00000000..d581a5c7 --- /dev/null +++ b/modules/option/init.test.luau @@ -0,0 +1,342 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local Option = require(script.Parent) + + ctx:Describe("Some", function() + ctx:Test("should create some option", function() + local opt = Option.Some(true) + ctx:Expect(opt:IsSome()):ToBe(true) + end) + + ctx:Test("should fail to create some option with nil", function() + ctx:Expect(function() + Option.Some(nil) + end):ToThrow() + end) + + ctx:Test("should not be none", function() + local opt = Option.Some(10) + ctx:Expect(opt:IsNone()):ToBe(false) + end) + end) + + ctx:Describe("None", function() + ctx:Test("should be able to reference none", function() + ctx:Expect(function() + local _none = Option.None + end) + :Not() + :ToThrow() + end) + + ctx:Test("should be able to check if none", function() + local none = Option.None + ctx:Expect(none:IsNone()):ToBe(true) + end) + + ctx:Test("should be able to check if not some", function() + local none = Option.None + ctx:Expect(none:IsSome()):ToBe(false) + end) + end) + + ctx:Describe("Equality", function() + ctx:Test("should equal the same some from same options", function() + local opt = Option.Some(32) + ctx:Expect(opt):ToBe(opt) + end) + + ctx:Test("should equal the same some from different options", function() + local opt1 = Option.Some(32) + local opt2 = Option.Some(32) + ctx:Expect(opt1):ToBe(opt2) + end) + end) + + ctx:Describe("Assert", function() + ctx:Test("should assert that a some option is an option", function() + ctx:Expect(Option.Is(Option.Some(10))):ToBe(true) + end) + + ctx:Test("should assert that a none option is an option", function() + ctx:Expect(Option.Is(Option.None)):ToBe(true) + end) + + ctx:Test("should assert that a non-option is not an option", function() + ctx:Expect(Option.Is(10)):ToBe(false) + ctx:Expect(Option.Is(true)):ToBe(false) + ctx:Expect(Option.Is(false)):ToBe(false) + ctx:Expect(Option.Is("Test")):ToBe(false) + ctx:Expect(Option.Is({})):ToBe(false) + ctx:Expect(Option.Is(function() end)):ToBe(false) + ctx:Expect(Option.Is(coroutine.create(function() end))):ToBe(false) + ctx:Expect(Option.Is(Option)):ToBe(false) + end) + end) + + ctx:Describe("Unwrap", function() + ctx:Test("should unwrap a some option", function() + local opt = Option.Some(10) + ctx:Expect(function() + opt:Unwrap() + end) + :Not() + :ToThrow() + ctx:Expect(opt:Unwrap()):ToBe(10) + end) + + ctx:Test("should fail to unwrap a none option", function() + local opt = Option.None + ctx:Expect(function() + opt:Unwrap() + end):ToThrow() + end) + end) + + ctx:Describe("Expect", function() + ctx:Test("should expect a some option", function() + local opt = Option.Some(10) + ctx:Expect(function() + opt:Expect("Expecting some value") + end) + :Not() + :ToThrow() + ctx:Expect(opt:Unwrap()):ToBe(10) + end) + + ctx:Test("should fail when expecting on a none option", function() + local opt = Option.None + ctx:Expect(function() + opt:Expect("Expecting some value") + end):ToThrow() + end) + end) + + ctx:Describe("ExpectNone", function() + ctx:Test("should fail to expect a none option", function() + local opt = Option.Some(10) + ctx:Expect(function() + opt:ExpectNone("Expecting some value") + end):ToThrow() + end) + + ctx:Test("should expect a none option", function() + local opt = Option.None + ctx:Expect(function() + opt:ExpectNone("Expecting some value") + end) + :Not() + :ToThrow() + end) + end) + + ctx:Describe("UnwrapOr", function() + ctx:Test("should unwrap a some option", function() + local opt = Option.Some(10) + ctx:Expect(opt:UnwrapOr(20)):ToBe(10) + end) + + ctx:Test("should unwrap a none option", function() + local opt = Option.None + ctx:Expect(opt:UnwrapOr(20)):ToBe(20) + end) + end) + + ctx:Describe("UnwrapOrElse", function() + ctx:Test("should unwrap a some option", function() + local opt = Option.Some(10) + local result = opt:UnwrapOrElse(function() + return 30 + end) + ctx:Expect(result):ToBe(10) + end) + + ctx:Test("should unwrap a none option", function() + local opt = Option.None + local result = opt:UnwrapOrElse(function() + return 30 + end) + ctx:Expect(result):ToBe(30) + end) + end) + + ctx:Describe("And", function() + ctx:Test("should return the second option with and when both are some", function() + local opt1 = Option.Some(1) + local opt2 = Option.Some(2) + ctx:Expect(opt1:And(opt2)):ToBe(opt2) + end) + + ctx:Test("should return none when first option is some and second option is none", function() + local opt1 = Option.Some(1) + local opt2 = Option.None + ctx:Expect(opt1:And(opt2):IsNone()):ToBe(true) + end) + + ctx:Test("should return none when first option is none and second option is some", function() + local opt1 = Option.None + local opt2 = Option.Some(2) + ctx:Expect(opt1:And(opt2):IsNone()):ToBe(true) + end) + + ctx:Test("should return none when both options are none", function() + local opt1 = Option.None + local opt2 = Option.None + ctx:Expect(opt1:And(opt2):IsNone()):ToBe(true) + end) + end) + + ctx:Describe("AndThen", function() + ctx:Test("should pass the some value to the predicate", function() + local opt = Option.Some(32) + opt:AndThen(function(value) + ctx:Expect(value):ToBe(32) + return Option.None + end) + end) + + ctx:Test("should throw if an option is not returned from predicate", function() + local opt = Option.Some(32) + ctx:Expect(function() + opt:AndThen(function() end) + end):ToThrow() + end) + + ctx:Test("should return none if the option is none", function() + local opt = Option.None + ctx:Expect(opt:AndThen(function() + return Option.Some(10) + end):IsNone()):ToBe(true) + end) + + ctx:Test("should return option of predicate if option is some", function() + local opt = Option.Some(32) + local result = opt:AndThen(function() + return Option.Some(10) + end) + ctx:Expect(result:IsSome()):ToBe(true) + ctx:Expect(result:Unwrap()):ToBe(10) + end) + end) + + ctx:Describe("Or", function() + ctx:Test("should return the first option if it is some", function() + local opt1 = Option.Some(10) + local opt2 = Option.Some(20) + ctx:Expect(opt1:Or(opt2)):ToBe(opt1) + end) + + ctx:Test("should return the second option if the first one is none", function() + local opt1 = Option.None + local opt2 = Option.Some(20) + ctx:Expect(opt1:Or(opt2)):ToBe(opt2) + end) + end) + + ctx:Describe("OrElse", function() + ctx:Test("should return the first option if it is some", function() + local opt1 = Option.Some(10) + local opt2 = Option.Some(20) + ctx:Expect(opt1:OrElse(function() + return opt2 + end)):ToBe(opt1) + end) + + ctx:Test("should return the second option if the first one is none", function() + local opt1 = Option.None + local opt2 = Option.Some(20) + ctx:Expect(opt1:OrElse(function() + return opt2 + end)):ToBe(opt2) + end) + + ctx:Test("should throw if the predicate does not return an option", function() + local opt1 = Option.None + ctx:Expect(function() + opt1:OrElse(function() end) + end):ToThrow() + end) + end) + + ctx:Describe("XOr", function() + ctx:Test("should return first option if first option is some and second option is none", function() + local opt1 = Option.Some(1) + local opt2 = Option.None + ctx:Expect(opt1:XOr(opt2)):ToBe(opt1) + end) + + ctx:Test("should return second option if first option is none and second option is some", function() + local opt1 = Option.None + local opt2 = Option.Some(2) + ctx:Expect(opt1:XOr(opt2)):ToBe(opt2) + end) + + ctx:Test("should return none if first and second option are some", function() + local opt1 = Option.Some(1) + local opt2 = Option.Some(2) + ctx:Expect(opt1:XOr(opt2)):ToBe(Option.None) + end) + + ctx:Test("should return none if first and second option are none", function() + local opt1 = Option.None + local opt2 = Option.None + ctx:Expect(opt1:XOr(opt2)):ToBe(Option.None) + end) + end) + + ctx:Describe("Filter", function() + ctx:Test("should return none if option is none", function() + local opt = Option.None + ctx:Expect(opt:Filter(function() end)):ToBe(Option.None) + end) + + ctx:Test("should return none if option is some but fails predicate", function() + local opt = Option.Some(10) + ctx:Expect(opt:Filter(function(_v) + return false + end)):ToBe(Option.None) + end) + + ctx:Test("should return self if option is some and passes predicate", function() + local opt = Option.Some(10) + ctx:Expect(opt:Filter(function(_v) + return true + end)):ToBe(opt) + end) + end) + + ctx:Describe("Contains", function() + ctx:Test("should return true if some option contains the given value", function() + local opt = Option.Some(32) + ctx:Expect(opt:Contains(32)):ToBe(true) + end) + + ctx:Test("should return false if some option does not contain the given value", function() + local opt = Option.Some(32) + ctx:Expect(opt:Contains(64)):ToBe(false) + end) + + ctx:Test("should return false if option is none", function() + local opt = Option.None + ctx:Expect(opt:Contains(64)):ToBe(false) + end) + end) + + ctx:Describe("ToString", function() + ctx:Test("should return string of none option", function() + local opt = Option.None + ctx:Expect(tostring(opt)):ToBe("Option") + end) + + ctx:Test("should return string of some option with type", function() + local values = { 10, true, false, "test", {}, function() end, coroutine.create(function() end), workspace } + for _, value in ipairs(values) do + local expectedString = ("Option<%s>"):format(typeof(value)) + ctx:Expect(tostring(Option.Some(value))):ToBe(expectedString) + end + end) + end) +end diff --git a/modules/ser/init.spec.luau b/modules/ser/init.spec.luau deleted file mode 100644 index c0710fea..00000000 --- a/modules/ser/init.spec.luau +++ /dev/null @@ -1,46 +0,0 @@ -return function() - local Ser = require(script.Parent) - local Option = require(script.Parent.Parent.Option) - - describe("SerializeArgs", function() - it("should serialize an option", function() - local opt = Option.Some(32) - local serOpt = table.unpack(Ser.SerializeArgs(opt)) - expect(serOpt.ClassName).to.equal("Option") - expect(serOpt.Value).to.equal(32) - end) - end) - - describe("SerializeArgsAndUnpack", function() - it("should serialize an option", function() - local opt = Option.Some(32) - local serOpt = Ser.SerializeArgsAndUnpack(opt) - expect(serOpt.ClassName).to.equal("Option") - expect(serOpt.Value).to.equal(32) - end) - end) - - describe("DeserializeArgs", function() - it("should deserialize args to option", function() - local serOpt = { - ClassName = "Option", - Value = 32, - } - local opt = table.unpack(Ser.DeserializeArgs(serOpt)) - expect(Option.Is(opt)).to.equal(true) - expect(opt:Contains(32)).to.equal(true) - end) - end) - - describe("DeserializeArgsAndUnpack", function() - it("should deserialize args to option", function() - local serOpt = { - ClassName = "Option", - Value = 32, - } - local opt = Ser.DeserializeArgsAndUnpack(serOpt) - expect(Option.Is(opt)).to.equal(true) - expect(opt:Contains(32)).to.equal(true) - end) - end) -end diff --git a/modules/ser/init.test.luau b/modules/ser/init.test.luau new file mode 100644 index 00000000..14773cb4 --- /dev/null +++ b/modules/ser/init.test.luau @@ -0,0 +1,50 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local Option = require(script.Parent.Parent.Option) + local Ser = require(script.Parent) + + ctx:Describe("SerializeArgs", function() + ctx:Test("should serialize an option", function() + local opt = Option.Some(32) + local serOpt = table.unpack(Ser.SerializeArgs(opt)) + ctx:Expect(serOpt.ClassName):ToBe("Option") + ctx:Expect(serOpt.Value):ToBe(32) + end) + end) + + ctx:Describe("SerializeArgsAndUnpack", function() + ctx:Test("should serialize an option", function() + local opt = Option.Some(32) + local serOpt = Ser.SerializeArgsAndUnpack(opt) + ctx:Expect(serOpt.ClassName):ToBe("Option") + ctx:Expect(serOpt.Value):ToBe(32) + end) + end) + + ctx:Describe("DeserializeArgs", function() + ctx:Test("should deserialize args to option", function() + local serOpt = { + ClassName = "Option", + Value = 32, + } + local opt = table.unpack(Ser.DeserializeArgs(serOpt)) + ctx:Expect(Option.Is(opt)):ToBe(true) + ctx:Expect(opt:Contains(32)):ToBe(true) + end) + end) + + ctx:Describe("DeserializeArgsAndUnpack", function() + ctx:Test("should deserialize args to option", function() + local serOpt = { + ClassName = "Option", + Value = 32, + } + local opt = Ser.DeserializeArgsAndUnpack(serOpt) + ctx:Expect(Option.Is(opt)):ToBe(true) + ctx:Expect(opt:Contains(32)):ToBe(true) + end) + end) +end diff --git a/modules/shake/init.spec.luau b/modules/shake/init.test.luau similarity index 53% rename from modules/shake/init.spec.luau rename to modules/shake/init.test.luau index c549a208..a336cc9b 100644 --- a/modules/shake/init.spec.luau +++ b/modules/shake/init.test.luau @@ -1,4 +1,7 @@ local RunService = game:GetService("RunService") +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) local function AwaitStop(shake): number local start = os.clock() @@ -10,43 +13,45 @@ local function AwaitStop(shake): number return os.clock() - start end -return function() +return function(ctx: Test.TestContext) local Shake = require(script.Parent) - describe("Construct", function() - it("should construct a new shake instance", function() - expect(function() + ctx:Describe("Construct", function() + ctx:Test("should construct a new shake instance", function() + ctx:Expect(function() local _shake = Shake.new() - end).to.never.throw() + end) + :Not() + :ToThrow() end) end) - describe("Static Functions", function() - it("should get next render name", function() + ctx:Describe("Static Functions", function() + ctx:Test("should get next render name", function() local r1 = Shake.NextRenderName() local r2 = Shake.NextRenderName() local r3 = Shake.NextRenderName() - expect(r1).to.be.a("string") - expect(r2).to.be.a("string") - expect(r3).to.be.a("string") - expect(r1).to.never.equal(r2) - expect(r2).to.never.equal(r3) - expect(r3).to.never.equal(r1) + ctx:Expect(r1):ToBeA("string") + ctx:Expect(r2):ToBeA("string") + ctx:Expect(r3):ToBeA("string") + ctx:Expect(r1):Not():ToBe(r2) + ctx:Expect(r2):Not():ToBe(r3) + ctx:Expect(r3):Not():ToBe(r1) end) - it("should perform inverse square", function() + ctx:Test("should perform inverse square", function() local vector = Vector3.new(10, 10, 10) local distance = 10 local expectedIntensity = 1 / (distance * distance) local expectedVector = vector * expectedIntensity local vectorInverseSq = Shake.InverseSquare(vector, distance) - expect(typeof(vectorInverseSq)).to.equal("Vector3") - expect(vectorInverseSq).to.equal(expectedVector) + ctx:Expect(typeof(vectorInverseSq)):ToBe("Vector3") + ctx:Expect(vectorInverseSq):ToBe(expectedVector) end) end) - describe("Cloning", function() - it("should clone a shake instance", function() + ctx:Describe("Cloning", function() + ctx:Test("should clone a shake instance", function() local shake1 = Shake.new() shake1.Amplitude = 5 shake1.Frequency = 2 @@ -60,9 +65,9 @@ return function() return os.clock() end local shake2 = shake1:Clone() - expect(shake2).to.be.a("table") - expect(getmetatable(shake2)).to.equal(Shake) - expect(shake2).to.never.equal(shake1) + ctx:Expect(shake2):ToBeA("table") + ctx:Expect(getmetatable(shake2)):ToBe(getmetatable(shake1)) + ctx:Expect(shake2):Not():ToBe(shake1) local clonedFields = { "Amplitude", "Frequency", @@ -72,119 +77,119 @@ return function() "Sustain", "PositionInfluence", "RotationInfluence", - "TimeFunction", } - for _, field in ipairs(clonedFields) do - expect(shake1[field]).to.equal(shake2[field]) + for _, field in clonedFields do + ctx:Expect(shake1[field]):ToBe(shake2[field]) end + ctx:Expect(shake1.TimeFunction == shake2.TimeFunction):ToBe(true) end) - it("should clone a shake instance but ignore running state", function() + ctx:Test("should clone a shake instance but ignore running state", function() local shake1 = Shake.new() shake1:Start() local shake2 = shake1:Clone() - expect(shake1:IsShaking()).to.equal(true) - expect(shake2:IsShaking()).to.equal(false) + ctx:Expect(shake1:IsShaking()):ToBe(true) + ctx:Expect(shake2:IsShaking()):ToBe(false) end) end) - describe("Shaking", function() - it("should start", function() + ctx:Describe("Shaking", function() + ctx:Test("should start", function() local shake = Shake.new() - expect(shake:IsShaking()).to.equal(false) + ctx:Expect(shake:IsShaking()):ToBe(false) shake:Start() - expect(shake:IsShaking()).to.equal(true) + ctx:Expect(shake:IsShaking()):ToBe(true) end) - it("should stop", function() + ctx:Test("should stop", function() local shake = Shake.new() shake:Start() - expect(shake:IsShaking()).to.equal(true) + ctx:Expect(shake:IsShaking()):ToBe(true) shake:Stop() - expect(shake:IsShaking()).to.equal(false) + ctx:Expect(shake:IsShaking()):ToBe(false) end) - it("should shake for nearly no time", function() + ctx:Test("should shake for nearly no time", function() local shake = Shake.new() shake.FadeInTime = 0 shake.FadeOutTime = 0 shake.SustainTime = 0 shake:Start() local duration = AwaitStop(shake) - expect(duration).to.be.near(0, 0.05) + ctx:Expect(duration):ToBeNear(0, 0.05) end) - it("should shake for fade in time", function() + ctx:Test("should shake for fade in time", function() local shake = Shake.new() shake.FadeInTime = 0.1 shake.FadeOutTime = 0 shake.SustainTime = 0 shake:Start() local duration = AwaitStop(shake) - expect(duration).to.be.near(0.1, 0.05) + ctx:Expect(duration):ToBeNear(0.1, 0.05) end) - it("should shake for fade out time", function() + ctx:Test("should shake for fade out time", function() local shake = Shake.new() shake.FadeInTime = 0 shake.FadeOutTime = 0.1 shake.SustainTime = 0 shake:Start() local duration = AwaitStop(shake) - expect(duration).to.be.near(0.1, 0.05) + ctx:Expect(duration):ToBeNear(0.1, 0.05) end) - it("should shake for sustain time", function() + ctx:Test("should shake for sustain time", function() local shake = Shake.new() shake.FadeInTime = 0 shake.FadeOutTime = 0 shake.SustainTime = 0.1 shake:Start() local duration = AwaitStop(shake) - expect(duration).to.be.near(0.1, 0.05) + ctx:Expect(duration):ToBeNear(0.1, 0.05) end) - it("should shake for fade in and sustain time", function() + ctx:Test("should shake for fade in and sustain time", function() local shake = Shake.new() shake.FadeInTime = 0.1 shake.FadeOutTime = 0 shake.SustainTime = 0.1 shake:Start() local duration = AwaitStop(shake) - expect(duration).to.be.near(0.2, 0.05) + ctx:Expect(duration):ToBeNear(0.2, 0.05) end) - it("should shake for fade out and sustain time", function() + ctx:Test("should shake for fade out and sustain time", function() local shake = Shake.new() shake.FadeInTime = 0 shake.FadeOutTime = 0.1 shake.SustainTime = 0.1 shake:Start() local duration = AwaitStop(shake) - expect(duration).to.be.near(0.2, 0.05) + ctx:Expect(duration):ToBeNear(0.2, 0.05) end) - it("should shake for fade in and fade out time", function() + ctx:Test("should shake for fade in and fade out time", function() local shake = Shake.new() shake.FadeInTime = 0.1 shake.FadeOutTime = 0.1 shake.SustainTime = 0 shake:Start() local duration = AwaitStop(shake) - expect(duration).to.be.near(0.2, 0.05) + ctx:Expect(duration):ToBeNear(0.2, 0.05) end) - it("should shake for fading and sustain time", function() + ctx:Test("should shake for fading and sustain time", function() local shake = Shake.new() shake.FadeInTime = 0.1 shake.FadeOutTime = 0.1 shake.SustainTime = 0.1 shake:Start() local duration = AwaitStop(shake) - expect(duration).to.be.near(0.3, 0.05) + ctx:Expect(duration):ToBeNear(0.3, 0.05) end) - it("should shake indefinitely", function() + ctx:Test("should shake indefinitely", function() local shake = Shake.new() shake.FadeInTime = 0 shake.FadeOutTime = 0 @@ -196,10 +201,10 @@ return function() shake:StopSustain() end) local duration = AwaitStop(shake) - expect(duration).to.be.near(shakeTime, 0.05) + ctx:Expect(duration):ToBeNear(shakeTime, 0.05) end) - it("should shake indefinitely and fade out", function() + ctx:Test("should shake indefinitely and fade out", function() local shake = Shake.new() shake.FadeInTime = 0 shake.FadeOutTime = 0.1 @@ -211,10 +216,10 @@ return function() shake:StopSustain() end) local duration = AwaitStop(shake) - expect(duration).to.be.near(0.2, 0.05) + ctx:Expect(duration):ToBeNear(0.2, 0.05) end) - it("should shake indefinitely and fade out with fade in time", function() + ctx:Test("should shake indefinitely and fade out with fade in time", function() local shake = Shake.new() shake.FadeInTime = 0.1 shake.FadeOutTime = 0.1 @@ -226,10 +231,10 @@ return function() shake:StopSustain() end) local duration = AwaitStop(shake) - expect(duration).to.be.near(0.4, 0.05) + ctx:Expect(duration):ToBeNear(0.4, 0.05) end) - it("should connect to signal", function() + ctx:Test("should connect to signal", function() local shake = Shake.new() shake.SustainTime = 0.1 shake:Start() @@ -237,23 +242,26 @@ return function() local connection = shake:OnSignal(RunService.Heartbeat, function() signaled = true end) - expect(typeof(connection)).to.equal("RBXScriptConnection") - expect(connection.Connected).to.equal(true) + ctx:Expect(typeof(connection)):ToBe("RBXScriptConnection") + ctx:Expect(connection.Connected):ToBe(true) AwaitStop(shake) - expect(signaled).to.equal(true) - expect(connection.Connected).to.equal(false) + ctx:Expect(signaled):ToBe(true) + ctx:Expect(connection.Connected):ToBe(false) end) - it("should bind to render step", function() - local shake = Shake.new() - shake.SustainTime = 0.1 - shake:Start() - local bound = false - shake:BindToRenderStep("ShakeTest", Enum.RenderPriority.Last.Value, function() - bound = true + -- RenderStepped only works on the client: + if RunService:IsClient() and RunService:IsRunning() then + ctx:Test("should bind to render step", function() + local shake = Shake.new() + shake.SustainTime = 0.1 + shake:Start() + local bound = false + shake:BindToRenderStep("ShakeTest", Enum.RenderPriority.Last.Value, function() + bound = true + end) + AwaitStop(shake) + ctx:Expect(bound):ToBe(true) end) - AwaitStop(shake) - expect(bound).to.equal(true) - end) + end end) end diff --git a/modules/signal/init.spec.luau b/modules/signal/init.spec.luau deleted file mode 100644 index 592a7f3a..00000000 --- a/modules/signal/init.spec.luau +++ /dev/null @@ -1,186 +0,0 @@ -local function AwaitCondition(predicate, timeout) - local start = os.clock() - timeout = (timeout or 10) - while true do - if predicate() then - return true - end - if (os.clock() - start) > timeout then - return false - end - task.wait() - end -end - -return function() - local Signal = require(script.Parent) - - local signal - - local function NumConns(sig) - sig = sig or signal - return #sig:GetConnections() - end - - beforeEach(function() - signal = Signal.new() - end) - - afterEach(function() - signal:Destroy() - end) - - describe("Constructor", function() - it("should create a new signal and fire it", function() - expect(Signal.Is(signal)).to.equal(true) - task.defer(function() - signal:Fire(10, 20) - end) - local n1, n2 = signal:Wait() - expect(n1).to.equal(10) - expect(n2).to.equal(20) - end) - - it("should create a proxy signal and connect to it", function() - local signalWrap = Signal.Wrap(game:GetService("RunService").Heartbeat) - expect(Signal.Is(signalWrap)).to.equal(true) - local fired = false - signalWrap:Connect(function() - fired = true - end) - expect(AwaitCondition(function() - return fired - end, 2)).to.equal(true) - signalWrap:Destroy() - end) - end) - - describe("FireDeferred", function() - it("should be able to fire primitive argument", function() - local send = 10 - local value - signal:Connect(function(v) - value = v - end) - signal:FireDeferred(send) - expect(AwaitCondition(function() - return (send == value) - end, 1)).to.equal(true) - end) - - it("should be able to fire a reference based argument", function() - local send = { 10, 20 } - local value - signal:Connect(function(v) - value = v - end) - signal:FireDeferred(send) - expect(AwaitCondition(function() - return (send == value) - end, 1)).to.equal(true) - end) - end) - - describe("Fire", function() - it("should be able to fire primitive argument", function() - local send = 10 - local value - signal:Connect(function(v) - value = v - end) - signal:Fire(send) - expect(value).to.equal(send) - end) - - it("should be able to fire a reference based argument", function() - local send = { 10, 20 } - local value - signal:Connect(function(v) - value = v - end) - signal:Fire(send) - expect(value).to.equal(send) - end) - end) - - describe("ConnectOnce", function() - it("should only capture first fire", function() - local value - local c = signal:ConnectOnce(function(v) - value = v - end) - expect(c.Connected).to.equal(true) - signal:Fire(10) - expect(c.Connected).to.equal(false) - signal:Fire(20) - expect(value).to.equal(10) - end) - end) - - describe("Wait", function() - it("should be able to wait for a signal to fire", function() - task.defer(function() - signal:Fire(10, 20, 30) - end) - local n1, n2, n3 = signal:Wait() - expect(n1).to.equal(10) - expect(n2).to.equal(20) - expect(n3).to.equal(30) - end) - end) - - describe("DisconnectAll", function() - it("should disconnect all connections", function() - signal:Connect(function() end) - signal:Connect(function() end) - expect(NumConns()).to.equal(2) - signal:DisconnectAll() - expect(NumConns()).to.equal(0) - end) - end) - - describe("Disconnect", function() - it("should disconnect connection", function() - local con = signal:Connect(function() end) - expect(NumConns()).to.equal(1) - con:Disconnect() - expect(NumConns()).to.equal(0) - end) - - it("should still work if connections disconnected while firing", function() - local a = 0 - local c - signal:Connect(function() - a += 1 - end) - c = signal:Connect(function() - c:Disconnect() - a += 1 - end) - signal:Connect(function() - a += 1 - end) - signal:Fire() - expect(a).to.equal(3) - end) - - it("should still work if connections disconnected while firing deferred", function() - local a = 0 - local c - signal:Connect(function() - a += 1 - end) - c = signal:Connect(function() - c:Disconnect() - a += 1 - end) - signal:Connect(function() - a += 1 - end) - signal:FireDeferred() - expect(AwaitCondition(function() - return a == 3 - end)).to.equal(true) - end) - end) -end diff --git a/modules/signal/init.test.luau b/modules/signal/init.test.luau new file mode 100644 index 00000000..c12c68a7 --- /dev/null +++ b/modules/signal/init.test.luau @@ -0,0 +1,190 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +local function AwaitCondition(predicate: () -> boolean, timeout: number?) + local start = os.clock() + timeout = (timeout or 10) + while true do + if predicate() then + return true + end + if (os.clock() - start) > timeout then + return false + end + task.wait() + end +end + +return function(ctx: Test.TestContext) + local Signal = require(script.Parent) + + local signal + + local function NumConns(sig) + sig = sig or signal + return #sig:GetConnections() + end + + ctx:BeforeEach(function() + signal = Signal.new() + end) + + ctx:AfterEach(function() + signal:Destroy() + end) + + ctx:Describe("Constructor", function() + ctx:Test("should create a new signal and fire it", function() + ctx:Expect(Signal.Is(signal)):ToBe(true) + task.defer(function() + signal:Fire(10, 20) + end) + local n1, n2 = signal:Wait() + ctx:Expect(n1):ToBe(10) + ctx:Expect(n2):ToBe(20) + end) + + ctx:Test("should create a proxy signal and connect to it", function() + local signalWrap = Signal.Wrap(game:GetService("RunService").Heartbeat) + ctx:Expect(Signal.Is(signalWrap)):ToBe(true) + local fired = false + signalWrap:Connect(function() + fired = true + end) + ctx:Expect(AwaitCondition(function() + return fired + end, 2)):ToBe(true) + signalWrap:Destroy() + end) + end) + + ctx:Describe("FireDeferred", function() + ctx:Test("should be able to fire primitive argument", function() + local send = 10 + local value + signal:Connect(function(v) + value = v + end) + signal:FireDeferred(send) + ctx:Expect(AwaitCondition(function() + return (send == value) + end, 1)):ToBe(true) + end) + + ctx:Test("should be able to fire a reference based argument", function() + local send = { 10, 20 } + local value + signal:Connect(function(v) + value = v + end) + signal:FireDeferred(send) + ctx:Expect(AwaitCondition(function() + return (send == value) + end, 1)):ToBe(true) + end) + end) + + ctx:Describe("Fire", function() + ctx:Test("should be able to fire primitive argument", function() + local send = 10 + local value + signal:Connect(function(v) + value = v + end) + signal:Fire(send) + ctx:Expect(value):ToBe(send) + end) + + ctx:Test("should be able to fire a reference based argument", function() + local send = { 10, 20 } + local value + signal:Connect(function(v) + value = v + end) + signal:Fire(send) + ctx:Expect(value):ToBe(send) + end) + end) + + ctx:Describe("ConnectOnce", function() + ctx:Test("should only capture first fire", function() + local value + local c = signal:ConnectOnce(function(v) + value = v + end) + ctx:Expect(c.Connected):ToBe(true) + signal:Fire(10) + ctx:Expect(c.Connected):ToBe(false) + signal:Fire(20) + ctx:Expect(value):ToBe(10) + end) + end) + + ctx:Describe("Wait", function() + ctx:Test("should be able to wait for a signal to fire", function() + task.defer(function() + signal:Fire(10, 20, 30) + end) + local n1, n2, n3 = signal:Wait() + ctx:Expect(n1):ToBe(10) + ctx:Expect(n2):ToBe(20) + ctx:Expect(n3):ToBe(30) + end) + end) + + ctx:Describe("DisconnectAll", function() + ctx:Test("should disconnect all connections", function() + signal:Connect(function() end) + signal:Connect(function() end) + ctx:Expect(NumConns()):ToBe(2) + signal:DisconnectAll() + ctx:Expect(NumConns()):ToBe(0) + end) + end) + + ctx:Describe("Disconnect", function() + ctx:Test("should disconnect connection", function() + local con = signal:Connect(function() end) + ctx:Expect(NumConns()):ToBe(1) + con:Disconnect() + ctx:Expect(NumConns()):ToBe(0) + end) + + ctx:Test("should still work if connections disconnected while firing", function() + local a = 0 + local c + signal:Connect(function() + a += 1 + end) + c = signal:Connect(function() + c:Disconnect() + a += 1 + end) + signal:Connect(function() + a += 1 + end) + signal:Fire() + ctx:Expect(a):ToBe(3) + end) + + ctx:Test("should still work if connections disconnected while firing deferred", function() + local a = 0 + local c + signal:Connect(function() + a += 1 + end) + c = signal:Connect(function() + c:Disconnect() + a += 1 + end) + signal:Connect(function() + a += 1 + end) + signal:FireDeferred() + ctx:Expect(AwaitCondition(function() + return a == 3 + end)):ToBe(true) + end) + end) +end diff --git a/modules/silo/init.spec.luau b/modules/silo/init.spec.luau deleted file mode 100644 index 7002f884..00000000 --- a/modules/silo/init.spec.luau +++ /dev/null @@ -1,209 +0,0 @@ -return function() - local Silo = require(script.Parent) - - local silo1, silo2, rootSilo - - beforeEach(function() - silo1 = Silo.new({ - Kills = 0, - Deaths = 0, - }, { - SetKills = function(state, kills) - state.Kills = kills - end, - IncrementDeaths = function(state, deaths) - state.Deaths += deaths - end, - }) - silo2 = Silo.new({ - Money = 0, - }, { - AddMoney = function(state, money) - state.Money += money - end, - }) - rootSilo = Silo.combine({ - Stats = silo1, - Econ = silo2, - }) - end) - - describe("State", function() - it("should get state properly", function() - local silo = Silo.new({ - ABC = 10, - }) - local state = silo:GetState() - expect(state).to.be.a("table") - expect(state.ABC).to.equal(10) - end) - - it("should get state from combined silos", function() - local state = rootSilo:GetState() - expect(state).to.be.a("table") - expect(state.Stats).to.be.a("table") - expect(state.Econ).to.be.a("table") - expect(state.Stats.Kills).to.be.a("number") - expect(state.Stats.Deaths).to.be.a("number") - expect(state.Econ.Money).to.be.a("number") - end) - - it("should not allow getting state from sub-silo", function() - expect(function() - silo1:GetState() - end).to.throw() - expect(function() - silo2:GetState() - end).to.throw() - end) - - it("should throw error if attempting to modify state directly", function() - expect(function() - rootSilo:GetState().Stats.Kills = 10 - end).to.throw() - expect(function() - rootSilo:GetState().Stats.SomethingNew = 100 - end).to.throw() - expect(function() - rootSilo:GetState().Stats = {} - end).to.throw() - expect(function() - rootSilo:GetState().SomethingElse = {} - end).to.throw() - end) - end) - - describe("Dispatch", function() - it("should dispatch", function() - expect(rootSilo:GetState().Stats.Kills).to.equal(0) - rootSilo:Dispatch(silo1.Actions.SetKills(10)) - expect(rootSilo:GetState().Stats.Kills).to.equal(10) - rootSilo:Dispatch(silo2.Actions.AddMoney(10)) - rootSilo:Dispatch(silo2.Actions.AddMoney(20)) - expect(rootSilo:GetState().Econ.Money).to.equal(30) - end) - - it("should not allow dispatching from a sub-silo", function() - expect(function() - silo1:Dispatch(silo1.Action.SetKills(0)) - end).to.throw() - expect(function() - silo2:Dispatch(silo2.Action.AddMoney(0)) - end).to.throw() - end) - - it("should not allow dispatching from within a modifier", function() - expect(function() - local silo - silo = Silo.new({ - Data = 0, - }, { - SetData = function(state, newData) - state.Data = newData - silo:Dispatch({ Name = "", Payload = 0 }) - end, - }) - silo:Dispatch(silo.Actions.SetData(0)) - end).to.throw() - end) - end) - - describe("Subscribe", function() - it("should subscribe to a silo", function() - local new, old - local n = 0 - local unsubscribe = rootSilo:Subscribe(function(newState, oldState) - n += 1 - new, old = newState, oldState - end) - expect(n).to.equal(0) - rootSilo:Dispatch(silo1.Actions.SetKills(10)) - expect(n).to.equal(1) - expect(new).to.be.a("table") - expect(old).to.be.a("table") - expect(new.Stats.Kills).to.equal(10) - expect(old.Stats.Kills).to.equal(0) - rootSilo:Dispatch(silo1.Actions.SetKills(20)) - expect(n).to.equal(2) - expect(new.Stats.Kills).to.equal(20) - expect(old.Stats.Kills).to.equal(10) - unsubscribe() - rootSilo:Dispatch(silo1.Actions.SetKills(30)) - expect(n).to.equal(2) - end) - - it("should not allow subscribing same function more than once", function() - local function sub() end - expect(function() - rootSilo:Subscribe(sub) - end).never.to.throw() - expect(function() - rootSilo:Subscribe(sub) - end).to.throw() - end) - - it("should not allow subscribing to a sub-silo", function() - expect(function() - silo1:Subscribe(function() end) - end).to.throw() - end) - - it("should not allow subscribing from within a modifier", function() - expect(function() - local silo - silo = Silo.new({ - Data = 0, - }, { - SetData = function(state, newData) - state.Data = newData - silo:Subscribe(function() end) - end, - }) - silo:Dispatch(silo.Actions.SetData(0)) - end).to.throw() - end) - end) - - describe("Watch", function() - it("should watch value changes", function() - local function SelectMoney(state) - return state.Econ.Money - end - local changes = 0 - local currentMoney = 0 - local unsubscribeWatch = rootSilo:Watch(SelectMoney, function(money) - changes += 1 - currentMoney = money - end) - expect(changes).to.equal(1) - rootSilo:Dispatch(silo2.Actions.AddMoney(10)) - expect(changes).to.equal(2) - expect(currentMoney).to.equal(10) - rootSilo:Dispatch(silo2.Actions.AddMoney(20)) - expect(changes).to.equal(3) - expect(currentMoney).to.equal(30) - rootSilo:Dispatch(silo2.Actions.AddMoney(0)) - expect(changes).to.equal(3) - expect(currentMoney).to.equal(30) - rootSilo:Dispatch(silo1.Actions.SetKills(10)) - expect(changes).to.equal(3) - expect(currentMoney).to.equal(30) - unsubscribeWatch() - rootSilo:Dispatch(silo2.Actions.AddMoney(10)) - expect(changes).to.equal(3) - expect(currentMoney).to.equal(30) - end) - end) - - describe("ResetToDefaultState", function() - it("should reset the silo to it's default state", function() - rootSilo:Dispatch(silo1.Actions.SetKills(10)) - rootSilo:Dispatch(silo2.Actions.AddMoney(30)) - expect(rootSilo:GetState().Stats.Kills).to.equal(10) - expect(rootSilo:GetState().Econ.Money).to.equal(30) - rootSilo:ResetToDefaultState() - expect(rootSilo:GetState().Stats.Kills).to.equal(0) - expect(rootSilo:GetState().Econ.Money).to.equal(0) - end) - end) -end diff --git a/modules/silo/init.test.luau b/modules/silo/init.test.luau new file mode 100644 index 00000000..2836f4be --- /dev/null +++ b/modules/silo/init.test.luau @@ -0,0 +1,215 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local Silo = require(script.Parent) + + local silo1, silo2, rootSilo + + ctx:BeforeEach(function() + silo1 = Silo.new({ + Kills = 0, + Deaths = 0, + }, { + SetKills = function(state, kills) + state.Kills = kills + end, + IncrementDeaths = function(state, deaths) + state.Deaths += deaths + end, + }) + silo2 = Silo.new({ + Money = 0, + }, { + AddMoney = function(state, money) + state.Money += money + end, + }) + rootSilo = Silo.combine({ + Stats = silo1, + Econ = silo2, + }) + end) + + ctx:Describe("State", function() + ctx:Test("should get state properly", function() + local silo = Silo.new({ + ABC = 10, + }) + local state = silo:GetState() + ctx:Expect(state):ToBeA("table") + ctx:Expect(state.ABC):ToBe(10) + end) + + ctx:Test("should get state from combined silos", function() + local state = rootSilo:GetState() + ctx:Expect(state):ToBeA("table") + ctx:Expect(state.Stats):ToBeA("table") + ctx:Expect(state.Econ):ToBeA("table") + ctx:Expect(state.Stats.Kills):ToBeA("number") + ctx:Expect(state.Stats.Deaths):ToBeA("number") + ctx:Expect(state.Econ.Money):ToBeA("number") + end) + + ctx:Test("should not allow getting state from sub-silo", function() + ctx:Expect(function() + silo1:GetState() + end):ToThrow() + ctx:Expect(function() + silo2:GetState() + end):ToThrow() + end) + + ctx:Test("should throw error if attempting to modify state directly", function() + ctx:Expect(function() + rootSilo:GetState().Stats.Kills = 10 + end):ToThrow() + ctx:Expect(function() + rootSilo:GetState().Stats.SomethingNew = 100 + end):ToThrow() + ctx:Expect(function() + rootSilo:GetState().Stats = {} + end):ToThrow() + ctx:Expect(function() + rootSilo:GetState().SomethingElse = {} + end):ToThrow() + end) + end) + + ctx:Describe("Dispatch", function() + ctx:Test("should dispatch", function() + ctx:Expect(rootSilo:GetState().Stats.Kills):ToBe(0) + rootSilo:Dispatch(silo1.Actions.SetKills(10)) + ctx:Expect(rootSilo:GetState().Stats.Kills):ToBe(10) + rootSilo:Dispatch(silo2.Actions.AddMoney(10)) + rootSilo:Dispatch(silo2.Actions.AddMoney(20)) + ctx:Expect(rootSilo:GetState().Econ.Money):ToBe(30) + end) + + ctx:Test("should not allow dispatching from a sub-silo", function() + ctx:Expect(function() + silo1:Dispatch(silo1.Action.SetKills(0)) + end):ToThrow() + ctx:Expect(function() + silo2:Dispatch(silo2.Action.AddMoney(0)) + end):ToThrow() + end) + + ctx:Test("should not allow dispatching from within a modifier", function() + ctx:Expect(function() + local silo + silo = Silo.new({ + Data = 0, + }, { + SetData = function(state, newData) + state.Data = newData + silo:Dispatch({ Name = "", Payload = 0 }) + end, + }) + silo:Dispatch(silo.Actions.SetData(0)) + end):ToThrow() + end) + end) + + ctx:Describe("Subscribe", function() + ctx:Test("should subscribe to a silo", function() + local new, old + local n = 0 + local unsubscribe = rootSilo:Subscribe(function(newState, oldState) + n += 1 + new, old = newState, oldState + end) + ctx:Expect(n):ToBe(0) + rootSilo:Dispatch(silo1.Actions.SetKills(10)) + ctx:Expect(n):ToBe(1) + ctx:Expect(new):ToBeA("table") + ctx:Expect(old):ToBeA("table") + ctx:Expect(new.Stats.Kills):ToBe(10) + ctx:Expect(old.Stats.Kills):ToBe(0) + rootSilo:Dispatch(silo1.Actions.SetKills(20)) + ctx:Expect(n):ToBe(2) + ctx:Expect(new.Stats.Kills):ToBe(20) + ctx:Expect(old.Stats.Kills):ToBe(10) + unsubscribe() + rootSilo:Dispatch(silo1.Actions.SetKills(30)) + ctx:Expect(n):ToBe(2) + end) + + ctx:Test("should not allow subscribing same function more than once", function() + local function sub() end + ctx:Expect(function() + rootSilo:Subscribe(sub) + end) + :Not() + :ToThrow() + ctx:Expect(function() + rootSilo:Subscribe(sub) + end):ToThrow() + end) + + ctx:Test("should not allow subscribing to a sub-silo", function() + ctx:Expect(function() + silo1:Subscribe(function() end) + end):ToThrow() + end) + + ctx:Test("should not allow subscribing from within a modifier", function() + ctx:Expect(function() + local silo + silo = Silo.new({ + Data = 0, + }, { + SetData = function(state, newData) + state.Data = newData + silo:Subscribe(function() end) + end, + }) + silo:Dispatch(silo.Actions.SetData(0)) + end):ToThrow() + end) + end) + + ctx:Describe("Watch", function() + ctx:Test("should watch value changes", function() + local function SelectMoney(state) + return state.Econ.Money + end + local changes = 0 + local currentMoney = 0 + local unsubscribeWatch = rootSilo:Watch(SelectMoney, function(money) + changes += 1 + currentMoney = money + end) + ctx:Expect(changes):ToBe(1) + rootSilo:Dispatch(silo2.Actions.AddMoney(10)) + ctx:Expect(changes):ToBe(2) + ctx:Expect(currentMoney):ToBe(10) + rootSilo:Dispatch(silo2.Actions.AddMoney(20)) + ctx:Expect(changes):ToBe(3) + ctx:Expect(currentMoney):ToBe(30) + rootSilo:Dispatch(silo2.Actions.AddMoney(0)) + ctx:Expect(changes):ToBe(3) + ctx:Expect(currentMoney):ToBe(30) + rootSilo:Dispatch(silo1.Actions.SetKills(10)) + ctx:Expect(changes):ToBe(3) + ctx:Expect(currentMoney):ToBe(30) + unsubscribeWatch() + rootSilo:Dispatch(silo2.Actions.AddMoney(10)) + ctx:Expect(changes):ToBe(3) + ctx:Expect(currentMoney):ToBe(30) + end) + end) + + ctx:Describe("ResetToDefaultState", function() + ctx:Test("should reset the silo to it's default state", function() + rootSilo:Dispatch(silo1.Actions.SetKills(10)) + rootSilo:Dispatch(silo2.Actions.AddMoney(30)) + ctx:Expect(rootSilo:GetState().Stats.Kills):ToBe(10) + ctx:Expect(rootSilo:GetState().Econ.Money):ToBe(30) + rootSilo:ResetToDefaultState() + ctx:Expect(rootSilo:GetState().Stats.Kills):ToBe(0) + ctx:Expect(rootSilo:GetState().Econ.Money):ToBe(0) + end) + end) +end diff --git a/modules/streamable/Streamable.spec.luau b/modules/streamable/Streamable.spec.luau deleted file mode 100644 index ee5a8510..00000000 --- a/modules/streamable/Streamable.spec.luau +++ /dev/null @@ -1,137 +0,0 @@ -return function() - local Streamable = require(script.Parent.Streamable) - - local instanceFolder - local instanceModel - - local function CreateInstance(name) - local folder = Instance.new("Folder") - folder.Name = name - folder.Archivable = false - folder.Parent = instanceFolder - return folder - end - - local function CreatePrimary() - local primary = Instance.new("Part") - primary.Anchored = true - primary.Parent = instanceModel - instanceModel.PrimaryPart = primary - return primary - end - - beforeAll(function() - instanceFolder = Instance.new("Folder") - instanceFolder.Name = "KnitTestFolder" - instanceFolder.Archivable = false - instanceFolder.Parent = workspace - instanceModel = Instance.new("Model") - instanceModel.Name = "KnitTestModel" - instanceModel.Archivable = false - instanceModel.Parent = workspace - end) - - afterEach(function() - instanceFolder:ClearAllChildren() - instanceModel:ClearAllChildren() - end) - - afterAll(function() - instanceFolder:Destroy() - instanceModel:Destroy() - end) - - describe("Streamable", function() - it("should detect instance that is immediately available", function() - local testInstance = CreateInstance("TestImmediate") - local streamable = Streamable.new(instanceFolder, "TestImmediate") - local observed = 0 - local cleaned = 0 - streamable:Observe(function(_instance, trove) - observed += 1 - trove:Add(function() - cleaned += 1 - end) - end) - task.wait() - testInstance.Parent = nil - task.wait() - testInstance.Parent = instanceFolder - task.wait() - streamable:Destroy() - task.wait() - expect(observed).to.equal(2) - expect(cleaned).to.equal(2) - end) - - it("should detect instance that is not immediately available", function() - local streamable = Streamable.new(instanceFolder, "TestImmediate") - local observed = 0 - local cleaned = 0 - streamable:Observe(function(_instance, trove) - observed += 1 - trove:Add(function() - cleaned += 1 - end) - end) - task.wait(0.1) - local testInstance = CreateInstance("TestImmediate") - task.wait() - testInstance.Parent = nil - task.wait() - testInstance.Parent = instanceFolder - task.wait() - streamable:Destroy() - task.wait() - expect(observed).to.equal(2) - expect(cleaned).to.equal(2) - end) - - it("should detect primary part that is immediately available", function() - local testInstance = CreatePrimary() - local streamable = Streamable.primary(instanceModel) - local observed = 0 - local cleaned = 0 - streamable:Observe(function(_instance, trove) - observed += 1 - trove:Add(function() - cleaned += 1 - end) - end) - task.wait() - testInstance.Parent = nil - task.wait() - testInstance.Parent = instanceModel - instanceModel.PrimaryPart = testInstance - task.wait() - streamable:Destroy() - task.wait() - expect(observed).to.equal(2) - expect(cleaned).to.equal(2) - end) - - it("should detect primary part that is not immediately available", function() - local streamable = Streamable.primary(instanceModel) - local observed = 0 - local cleaned = 0 - streamable:Observe(function(_instance, trove) - observed += 1 - trove:Add(function() - cleaned += 1 - end) - end) - task.wait(0.1) - local testInstance = CreatePrimary() - task.wait() - testInstance.Parent = nil - task.wait() - testInstance.Parent = instanceModel - instanceModel.PrimaryPart = testInstance - task.wait() - streamable:Destroy() - task.wait() - expect(observed).to.equal(2) - expect(cleaned).to.equal(2) - end) - end) -end diff --git a/modules/streamable/StreamableUtil.spec.luau b/modules/streamable/StreamableUtil.spec.luau deleted file mode 100644 index 00c266b3..00000000 --- a/modules/streamable/StreamableUtil.spec.luau +++ /dev/null @@ -1,60 +0,0 @@ -return function() - local Streamable = require(script.Parent.Streamable) - local StreamableUtil = require(script.Parent.StreamableUtil) - - local instanceFolder - - local function CreateInstance(name) - local folder = Instance.new("Folder") - folder.Name = name - folder.Archivable = false - folder.Parent = instanceFolder - return folder - end - - beforeAll(function() - instanceFolder = Instance.new("Folder") - instanceFolder.Name = "KnitTest" - instanceFolder.Archivable = false - instanceFolder.Parent = workspace - end) - - afterEach(function() - instanceFolder:ClearAllChildren() - end) - - afterAll(function() - instanceFolder:Destroy() - end) - - describe("Compound", function() - it("should capture multiple streams", function() - local s1 = Streamable.new(instanceFolder, "ABC") - local s2 = Streamable.new(instanceFolder, "XYZ") - local observe = 0 - local cleaned = 0 - StreamableUtil.Compound({ S1 = s1, S2 = s2 }, function(_streamables, trove) - observe += 1 - trove:Add(function() - cleaned += 1 - end) - end) - local i1 = CreateInstance("ABC") - local i2 = CreateInstance("XYZ") - task.wait() - i1.Parent = nil - task.wait() - i1.Parent = instanceFolder - task.wait() - i1.Parent = nil - i2.Parent = nil - task.wait() - i2.Parent = instanceFolder - task.wait() - expect(observe).to.equal(2) - expect(cleaned).to.equal(2) - s1:Destroy() - s2:Destroy() - end) - end) -end diff --git a/modules/symbol/init.spec.luau b/modules/symbol/init.spec.luau deleted file mode 100644 index a8b4f580..00000000 --- a/modules/symbol/init.spec.luau +++ /dev/null @@ -1,33 +0,0 @@ -return function() - local Symbol = require(script.Parent) - - describe("Constructor", function() - it("should create a new symbol", function() - local symbol = Symbol("Test") - expect(symbol).to.be.a("userdata") - expect(symbol == symbol).to.equal(true) - expect(tostring(symbol)).to.equal("Symbol(Test)") - end) - - it("should create a new symbol with no name", function() - local symbol = Symbol() - expect(symbol).to.be.a("userdata") - expect(symbol == symbol).to.equal(true) - expect(tostring(symbol)).to.equal("Symbol()") - end) - - it("should be unique regardless of the name", function() - expect(Symbol("Test") == Symbol("Test")).to.equal(false) - expect(Symbol() == Symbol()).to.equal(false) - expect(Symbol("Test") == Symbol()).to.equal(false) - expect(Symbol("Test1") == Symbol("Test2")).to.equal(false) - end) - - it("should be useable as a table key", function() - local symbol = Symbol() - local t = {} - t[symbol] = 100 - expect(t[symbol]).to.equal(100) - end) - end) -end diff --git a/modules/symbol/init.test.luau b/modules/symbol/init.test.luau new file mode 100644 index 00000000..09fb9326 --- /dev/null +++ b/modules/symbol/init.test.luau @@ -0,0 +1,37 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local Symbol = require(script.Parent) + + ctx:Describe("Constructor", function() + ctx:Test("should create a new symbol", function() + local symbol = Symbol("Test") + ctx:Expect(symbol):ToBeA("userdata") + ctx:Expect(symbol == symbol):ToBe(true) + ctx:Expect(tostring(symbol)):ToBe("Symbol(Test)") + end) + + ctx:Test("should create a new symbol with no name", function() + local symbol = Symbol() + ctx:Expect(symbol):ToBeA("userdata") + ctx:Expect(symbol == symbol):ToBe(true) + ctx:Expect(tostring(symbol)):ToBe("Symbol()") + end) + + ctx:Test("should be unique regardless of the name", function() + ctx:Expect(Symbol("Test") == Symbol("Test")):ToBe(false) + ctx:Expect(Symbol() == Symbol()):ToBe(false) + ctx:Expect(Symbol("Test") == Symbol()):ToBe(false) + ctx:Expect(Symbol("Test1") == Symbol("Test2")):ToBe(false) + end) + + ctx:Test("should be useable as a table key", function() + local symbol = Symbol() + local t = {} + t[symbol] = 100 + ctx:Expect(t[symbol]):ToBe(100) + end) + end) +end diff --git a/modules/table-util/init.spec.luau b/modules/table-util/init.spec.luau deleted file mode 100644 index 54da665c..00000000 --- a/modules/table-util/init.spec.luau +++ /dev/null @@ -1,427 +0,0 @@ -return function() - local TableUtil = require(script.Parent) - - describe("Copy (Deep)", function() - it("should create a deep table copy", function() - local tbl = { a = { b = { c = { d = 32 } } } } - local tblCopy = TableUtil.Copy(tbl, true) - expect(tbl).never.to.equal(tblCopy) - expect(tbl.a).never.to.equal(tblCopy.a) - expect(tblCopy.a.b.c.d).to.equal(tbl.a.b.c.d) - end) - end) - - describe("Copy (Shallow)", function() - it("should create a shallow dictionary copy", function() - local tbl = { a = { b = { c = { d = 32 } } } } - local tblCopy = TableUtil.Copy(tbl) - expect(tblCopy).never.to.equal(tbl) - expect(tblCopy.a).to.equal(tbl.a) - expect(tblCopy.a.b.c.d).to.equal(tbl.a.b.c.d) - end) - - it("should create a shallow array copy", function() - local tbl = { 10, 20, 30, 40 } - local tblCopy = TableUtil.Copy(tbl) - expect(tblCopy).never.to.equal(tbl) - for i, v in ipairs(tbl) do - expect(tblCopy[i]).to.equal(v) - end - end) - end) - - describe("Sync", function() - it("should sync tables", function() - local template = { a = 32, b = 64, c = 128, e = { h = 1 } } - local tblSrc = { a = 32, b = 10, d = 1, e = { h = 2, n = 2 }, f = { x = 10 } } - local tbl = TableUtil.Sync(tblSrc, template) - expect(tbl.a).to.equal(template.a) - expect(tbl.b).to.equal(10) - expect(tbl.c).to.equal(template.c) - expect(tbl.d).never.to.be.ok() - expect(tbl.e.h).to.equal(2) - expect(tbl.e.n).never.to.be.ok() - expect(tbl.f).never.to.be.ok() - end) - end) - - describe("Reconcile", function() - it("should reconcile table", function() - local template = { kills = 0, deaths = 0, xp = 10, stuff = {}, stuff2 = "abc", stuff3 = { "data" } } - local data = - { kills = 10, deaths = 4, stuff = { "abc", "xyz" }, extra = 5, stuff2 = { abc = 10 }, stuff3 = true } - local reconciled = TableUtil.Reconcile(data, template) - expect(reconciled).never.to.equal(data) - expect(reconciled).never.to.equal(template) - expect(reconciled.kills).to.equal(10) - expect(reconciled.deaths).to.equal(4) - expect(reconciled.xp).to.equal(10) - expect(reconciled.stuff[1]).to.equal("abc") - expect(reconciled.stuff[2]).to.equal("xyz") - expect(reconciled.extra).to.equal(5) - expect(type(reconciled.stuff2)).to.equal("table") - expect(reconciled.stuff2).never.to.equal(data.stuff2) - expect(reconciled.stuff2.abc).to.equal(10) - expect(type(reconciled.stuff3)).to.equal("boolean") - expect(reconciled.stuff3).to.equal(true) - end) - end) - - describe("SwapRemove", function() - it("should swap remove index", function() - local tbl = { 1, 2, 3, 4, 5 } - TableUtil.SwapRemove(tbl, 3) - expect(#tbl).to.equal(4) - expect(tbl[3]).to.equal(5) - end) - end) - - describe("SwapRemoveFirstValue", function() - it("should swap remove first value given", function() - local tbl = { "hello", "world", "goodbye", "planet" } - TableUtil.SwapRemoveFirstValue(tbl, "world") - expect(#tbl).to.equal(3) - expect(tbl[2]).to.equal("planet") - end) - end) - - describe("Map", function() - it("should map table", function() - local tbl = { - { FirstName = "John", LastName = "Doe" }, - { FirstName = "Jane", LastName = "Smith" }, - } - local tblMapped = TableUtil.Map(tbl, function(person) - return person.FirstName .. " " .. person.LastName - end) - expect(tblMapped[1]).to.equal("John Doe") - expect(tblMapped[2]).to.equal("Jane Smith") - end) - end) - - describe("Filter", function() - it("should filter table", function() - local tbl = { 10, 20, 30, 40, 50, 60, 70, 80, 90 } - local tblFiltered = TableUtil.Filter(tbl, function(n) - return (n >= 30 and n <= 60) - end) - expect(#tblFiltered).to.equal(4) - expect(tblFiltered[1]).to.equal(30) - expect(tblFiltered[#tblFiltered]).to.equal(60) - end) - end) - - describe("Reduce", function() - it("should reduce table with numbers", function() - local tbl = { 1, 2, 3, 4, 5 } - local reduced = TableUtil.Reduce(tbl, function(accum, value) - return accum + value - end) - expect(reduced).to.equal(15) - end) - - it("should reduce table", function() - local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } } - local reduced = TableUtil.Reduce(tbl, function(accum, value) - return accum + value.Score - end, 0) - expect(reduced).to.equal(60) - end) - - it("should reduce table with initial value", function() - local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } } - local reduced = TableUtil.Reduce(tbl, function(accum, value) - return accum + value.Score - end, 40) - expect(reduced).to.equal(100) - end) - - it("should reduce functions", function() - local function Square(x) - return x * x - end - local function Double(x) - return x * 2 - end - local Func = TableUtil.Reduce({ Square, Double }, function(a, b) - return function(x) - return a(b(x)) - end - end) - local result = Func(10) - expect(result).to.equal(400) - end) - end) - - describe("Assign", function() - it("should assign tables", function() - local target = { a = 32, x = 100 } - local t1 = { b = 64, c = 128 } - local t2 = { a = 10, c = 100, d = 200 } - local tbl = TableUtil.Assign(target, t1, t2) - expect(tbl.a).to.equal(10) - expect(tbl.b).to.equal(64) - expect(tbl.c).to.equal(100) - expect(tbl.d).to.equal(200) - expect(tbl.x).to.equal(100) - end) - end) - - describe("Extend", function() - it("should extend tables", function() - local tbl = { "a", "b", "c" } - local extension = { "d", "e", "f" } - local extended = TableUtil.Extend(tbl, extension) - expect(table.concat(extended)).to.equal("abcdef") - end) - end) - - describe("Reverse", function() - it("should create a table in reverse", function() - local tbl = { 1, 2, 3 } - local tblRev = TableUtil.Reverse(tbl) - expect(table.concat(tblRev)).to.equal("321") - end) - end) - - describe("Shuffle", function() - it("should shuffle a table", function() - local tbl = { 1, 2, 3, 4, 5 } - expect(function() - TableUtil.Shuffle(tbl) - end).never.to.throw() - end) - end) - - describe("Sample", function() - it("should sample a table", function() - local tbl = { 1, 2, 3, 4, 5 } - local sample = TableUtil.Sample(tbl, 3) - expect(#sample).to.equal(3) - end) - end) - - describe("Flat", function() - it("should flatten table", function() - local tbl = { 1, 2, 3, { 4, 5, { 6, 7 } } } - local tblFlat = TableUtil.Flat(tbl, 3) - expect(table.concat(tblFlat)).to.equal("1234567") - end) - end) - - describe("FlatMap", function() - it("should map and flatten table", function() - local tbl = { 1, 2, 3, 4, 5, 6, 7 } - local tblFlat = TableUtil.FlatMap(tbl, function(n) - return { n, n * 2 } - end) - expect(table.concat(tblFlat)).to.equal("12243648510612714") - end) - end) - - describe("Keys", function() - it("should give all keys of table", function() - local tbl = { a = 1, b = 2, c = 3 } - local keys = TableUtil.Keys(tbl) - expect(#keys).to.equal(3) - expect(table.find(keys, "a")).to.be.ok() - expect(table.find(keys, "b")).to.be.ok() - expect(table.find(keys, "c")).to.be.ok() - end) - end) - - describe("Values", function() - it("should give all values of table", function() - local tbl = { a = 1, b = 2, c = 3 } - local values = TableUtil.Values(tbl) - expect(#values).to.equal(3) - expect(table.find(values, 1)).to.be.ok() - expect(table.find(values, 2)).to.be.ok() - expect(table.find(values, 3)).to.be.ok() - end) - end) - - describe("Find", function() - it("should find item in array", function() - local tbl = { 10, 20, 30 } - local item, index = TableUtil.Find(tbl, function(value) - return (value == 20) - end) - expect(item).to.be.ok() - expect(index).to.equal(2) - expect(item).to.equal(20) - end) - - it("should find item in dictionary", function() - local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } } - local item, index = TableUtil.Find(tbl, function(value) - return (value.Score == 20) - end) - expect(item).to.be.ok() - expect(index).to.equal(2) - expect(item.Score).to.equal(20) - end) - end) - - describe("Every", function() - it("should see every value is above 20", function() - local tbl = { 21, 40, 200 } - local every = TableUtil.Every(tbl, function(n) - return (n > 20) - end) - expect(every).to.equal(true) - end) - - it("should see every value is not above 20", function() - local tbl = { 20, 40, 200 } - local every = TableUtil.Every(tbl, function(n) - return (n > 20) - end) - expect(every).never.to.equal(true) - end) - end) - - describe("Some", function() - it("should see some value is above 20", function() - local tbl = { 5, 40, 1 } - local every = TableUtil.Some(tbl, function(n) - return (n > 20) - end) - expect(every).to.equal(true) - end) - - it("should see some value is not above 20", function() - local tbl = { 5, 15, 1 } - local every = TableUtil.Some(tbl, function(n) - return (n > 20) - end) - expect(every).never.to.equal(true) - end) - end) - - describe("Truncate", function() - it("should truncate an array", function() - local t1 = { 1, 2, 3, 4, 5 } - local t2 = TableUtil.Truncate(t1, 3) - expect(#t2).to.equal(3) - expect(t2[1]).to.equal(t1[1]) - expect(t2[2]).to.equal(t1[2]) - expect(t2[3]).to.equal(t1[3]) - end) - - it("should truncate an array with out of bounds sizes", function() - local t1 = { 1, 2, 3, 4, 5 } - expect(function() - TableUtil.Truncate(t1, -1) - end).to.never.throw() - expect(function() - TableUtil.Truncate(t1, #t1 + 1) - end).to.never.throw() - local t2 = TableUtil.Truncate(t1, #t1 + 10) - expect(#t2).to.equal(#t1) - expect(t2).to.never.equal(t1) - end) - end) - - describe("Lock", function() - it("should lock a table", function() - local t = { abc = { xyz = { num = 32 } } } - expect(function() - t.abc.xyz.num = 64 - end).never.to.throw() - local t2 = TableUtil.Lock(t) - expect(t.abc.xyz.num).to.equal(64) - expect(t).to.equal(t2) - expect(function() - t.abc.xyz.num = 10 - end).to.throw() - end) - end) - - describe("Zip", function() - it("should zip arrays together", function() - local t1 = { 1, 2, 3, 4, 5 } - local t2 = { 9, 8, 7, 6, 5 } - local t3 = { 1, 1, 1, 1, 1 } - local lastIndex = 0 - for i, v in TableUtil.Zip(t1, t2, t3) do - lastIndex = i - expect(v[1]).to.equal(t1[i]) - expect(v[2]).to.equal(t2[i]) - expect(v[3]).to.equal(t3[i]) - end - expect(lastIndex).to.equal(math.min(#t1, #t2, #t3)) - end) - - it("should zip arrays of different lengths together", function() - local t1 = { 1, 2, 3, 4, 5 } - local t2 = { 9, 8, 7, 6 } - local t3 = { 1, 1, 1 } - local lastIndex = 0 - for i, v in TableUtil.Zip(t1, t2, t3) do - lastIndex = i - expect(v[1]).to.equal(t1[i]) - expect(v[2]).to.equal(t2[i]) - expect(v[3]).to.equal(t3[i]) - end - expect(lastIndex).to.equal(math.min(#t1, #t2, #t3)) - end) - - it("should zip maps together", function() - local t1 = { a = 10, b = 20, c = 30 } - local t2 = { a = 100, b = 200, c = 300 } - local t3 = { a = 3000, b = 2000, c = 3000 } - for k, v in TableUtil.Zip(t1, t2, t3) do - expect(v[1]).to.equal(t1[k]) - expect(v[2]).to.equal(t2[k]) - expect(v[3]).to.equal(t3[k]) - end - end) - - it("should zip maps of different keys together", function() - local t1 = { a = 10, b = 20, c = 30, d = 40 } - local t2 = { a = 100, b = 200, c = 300, z = 10 } - local t3 = { a = 3000, b = 2000, c = 3000, x = 0 } - for k, v in TableUtil.Zip(t1, t2, t3) do - expect(v[1]).to.equal(t1[k]) - expect(v[2]).to.equal(t2[k]) - expect(v[3]).to.equal(t3[k]) - end - end) - end) - - describe("IsEmpty", function() - it("should detect that table is empty", function() - local tbl = {} - local isEmpty = TableUtil.IsEmpty(tbl) - expect(isEmpty).to.equal(true) - end) - - it("should detect that array is not empty", function() - local tbl = { 10, 20, 30 } - local isEmpty = TableUtil.IsEmpty(tbl) - expect(isEmpty).to.equal(false) - end) - - it("should detect that dictionary is not empty", function() - local tbl = { a = 10, b = 20, c = 30 } - local isEmpty = TableUtil.IsEmpty(tbl) - expect(isEmpty).to.equal(false) - end) - end) - - describe("JSON", function() - it("should encode json", function() - local tbl = { hello = "world" } - local json = TableUtil.EncodeJSON(tbl) - expect(json).to.equal('{"hello":"world"}') - end) - - it("should decode json", function() - local json = '{"hello":"world"}' - local tbl = TableUtil.DecodeJSON(json) - expect(tbl).to.be.a("table") - expect(tbl.hello).to.equal("world") - end) - end) -end diff --git a/modules/table-util/init.test.luau b/modules/table-util/init.test.luau new file mode 100644 index 00000000..452745ba --- /dev/null +++ b/modules/table-util/init.test.luau @@ -0,0 +1,439 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local TableUtil = require(script.Parent) + + ctx:Describe("Copy (Deep)", function() + ctx:Test("should create a deep table copy", function() + local tbl = { a = { b = { c = { d = 32 } } } } + local tblCopy = TableUtil.Copy(tbl, true) + ctx:Expect(tbl):Not():ToBe(tblCopy) + ctx:Expect(tbl.a):Not():ToBe(tblCopy.a) + ctx:Expect(tblCopy.a.b.c.d):ToBe(tbl.a.b.c.d) + end) + end) + + ctx:Describe("Copy (Shallow)", function() + ctx:Test("should create a shallow dictionary copy", function() + local tbl = { a = { b = { c = { d = 32 } } } } + local tblCopy = TableUtil.Copy(tbl) + ctx:Expect(tblCopy):Not():ToBe(tbl) + ctx:Expect(tblCopy.a):ToBe(tbl.a) + ctx:Expect(tblCopy.a.b.c.d):ToBe(tbl.a.b.c.d) + end) + + ctx:Test("should create a shallow array copy", function() + local tbl = { 10, 20, 30, 40 } + local tblCopy = TableUtil.Copy(tbl) + ctx:Expect(tblCopy):Not():ToBe(tbl) + for i, v in ipairs(tbl) do + ctx:Expect(tblCopy[i]):ToBe(v) + end + end) + end) + + ctx:Describe("Sync", function() + ctx:Test("should sync tables", function() + local template = { a = 32, b = 64, c = 128, e = { h = 1 } } + local tblSrc = { a = 32, b = 10, d = 1, e = { h = 2, n = 2 }, f = { x = 10 } } + local tbl = TableUtil.Sync(tblSrc, template) + ctx:Expect(tbl.a):ToBe(template.a) + ctx:Expect(tbl.b):ToBe(10) + ctx:Expect(tbl.c):ToBe(template.c) + ctx:Expect(tbl.d):ToBeNil() + ctx:Expect(tbl.e.h):ToBe(2) + ctx:Expect(tbl.e.n):ToBeNil() + ctx:Expect(tbl.f):ToBeNil() + end) + end) + + ctx:Describe("Reconcile", function() + ctx:Test("should reconcile table", function() + local template = { kills = 0, deaths = 0, xp = 10, stuff = {}, stuff2 = "abc", stuff3 = { "data" } } + local data = + { kills = 10, deaths = 4, stuff = { "abc", "xyz" }, extra = 5, stuff2 = { abc = 10 }, stuff3 = true } + local reconciled = TableUtil.Reconcile(data, template) + ctx:Expect(reconciled):Not():ToBe(data) + ctx:Expect(reconciled):Not():ToBe(template) + ctx:Expect(reconciled.kills):ToBe(10) + ctx:Expect(reconciled.deaths):ToBe(4) + ctx:Expect(reconciled.xp):ToBe(10) + ctx:Expect(reconciled.stuff[1]):ToBe("abc") + ctx:Expect(reconciled.stuff[2]):ToBe("xyz") + ctx:Expect(reconciled.extra):ToBe(5) + ctx:Expect(type(reconciled.stuff2)):ToBe("table") + ctx:Expect(reconciled.stuff2):Not():ToBe(data.stuff2) + ctx:Expect(reconciled.stuff2.abc):ToBe(10) + ctx:Expect(type(reconciled.stuff3)):ToBe("boolean") + ctx:Expect(reconciled.stuff3):ToBe(true) + end) + end) + + ctx:Describe("SwapRemove", function() + ctx:Test("should swap remove index", function() + local tbl = { 1, 2, 3, 4, 5 } + TableUtil.SwapRemove(tbl, 3) + ctx:Expect(#tbl):ToBe(4) + ctx:Expect(tbl[3]):ToBe(5) + end) + end) + + ctx:Describe("SwapRemoveFirstValue", function() + ctx:Test("should swap remove first value given", function() + local tbl = { "hello", "world", "goodbye", "planet" } + TableUtil.SwapRemoveFirstValue(tbl, "world") + ctx:Expect(#tbl):ToBe(3) + ctx:Expect(tbl[2]):ToBe("planet") + end) + end) + + ctx:Describe("Map", function() + ctx:Test("should map table", function() + local tbl = { + { FirstName = "John", LastName = "Doe" }, + { FirstName = "Jane", LastName = "Smith" }, + } + local tblMapped = TableUtil.Map(tbl, function(person) + return person.FirstName .. " " .. person.LastName + end) + ctx:Expect(tblMapped[1]):ToBe("John Doe") + ctx:Expect(tblMapped[2]):ToBe("Jane Smith") + end) + end) + + ctx:Describe("Filter", function() + ctx:Test("should filter table", function() + local tbl = { 10, 20, 30, 40, 50, 60, 70, 80, 90 } + local tblFiltered = TableUtil.Filter(tbl, function(n) + return (n >= 30 and n <= 60) + end) + ctx:Expect(#tblFiltered):ToBe(4) + ctx:Expect(tblFiltered[1]):ToBe(30) + ctx:Expect(tblFiltered[#tblFiltered]):ToBe(60) + end) + end) + + ctx:Describe("Reduce", function() + ctx:Test("should reduce table with numbers", function() + local tbl = { 1, 2, 3, 4, 5 } + local reduced = TableUtil.Reduce(tbl, function(accum, value) + return accum + value + end) + ctx:Expect(reduced):ToBe(15) + end) + + ctx:Test("should reduce table", function() + local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } } + local reduced = TableUtil.Reduce(tbl, function(accum, value) + return accum + value.Score + end, 0) + ctx:Expect(reduced):ToBe(60) + end) + + ctx:Test("should reduce table with initial value", function() + local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } } + local reduced = TableUtil.Reduce(tbl, function(accum, value) + return accum + value.Score + end, 40) + ctx:Expect(reduced):ToBe(100) + end) + + ctx:Test("should reduce functions", function() + local function Square(x) + return x * x + end + local function Double(x) + return x * 2 + end + local Func = TableUtil.Reduce({ Square, Double }, function(a, b) + return function(x) + return a(b(x)) + end + end) + local result = Func(10) + ctx:Expect(result):ToBe(400) + end) + end) + + ctx:Describe("Assign", function() + ctx:Test("should assign tables", function() + local target = { a = 32, x = 100 } + local t1 = { b = 64, c = 128 } + local t2 = { a = 10, c = 100, d = 200 } + local tbl = TableUtil.Assign(target, t1, t2) + ctx:Expect(tbl.a):ToBe(10) + ctx:Expect(tbl.b):ToBe(64) + ctx:Expect(tbl.c):ToBe(100) + ctx:Expect(tbl.d):ToBe(200) + ctx:Expect(tbl.x):ToBe(100) + end) + end) + + ctx:Describe("Extend", function() + ctx:Test("should extend tables", function() + local tbl = { "a", "b", "c" } + local extension = { "d", "e", "f" } + local extended = TableUtil.Extend(tbl, extension) + ctx:Expect(table.concat(extended)):ToBe("abcdef") + end) + end) + + ctx:Describe("Reverse", function() + ctx:Test("should create a table in reverse", function() + local tbl = { 1, 2, 3 } + local tblRev = TableUtil.Reverse(tbl) + ctx:Expect(table.concat(tblRev)):ToBe("321") + end) + end) + + ctx:Describe("Shuffle", function() + ctx:Test("should shuffle a table", function() + local tbl = { 1, 2, 3, 4, 5 } + ctx:Expect(function() + TableUtil.Shuffle(tbl) + end) + :Not() + :ToThrow() + end) + end) + + ctx:Describe("Sample", function() + ctx:Test("should sample a table", function() + local tbl = { 1, 2, 3, 4, 5 } + local sample = TableUtil.Sample(tbl, 3) + ctx:Expect(#sample):ToBe(3) + end) + end) + + ctx:Describe("Flat", function() + ctx:Test("should flatten table", function() + local tbl = { 1, 2, 3, { 4, 5, { 6, 7 } } } + local tblFlat = TableUtil.Flat(tbl, 3) + ctx:Expect(table.concat(tblFlat)):ToBe("1234567") + end) + end) + + ctx:Describe("FlatMap", function() + ctx:Test("should map and flatten table", function() + local tbl = { 1, 2, 3, 4, 5, 6, 7 } + local tblFlat = TableUtil.FlatMap(tbl, function(n) + return { n, n * 2 } + end) + ctx:Expect(table.concat(tblFlat)):ToBe("12243648510612714") + end) + end) + + ctx:Describe("Keys", function() + ctx:Test("should give all keys of table", function() + local tbl = { a = 1, b = 2, c = 3 } + local keys = TableUtil.Keys(tbl) + ctx:Expect(#keys):ToBe(3) + ctx:Expect(table.find(keys, "a")):ToBeOk() + ctx:Expect(table.find(keys, "b")):ToBeOk() + ctx:Expect(table.find(keys, "c")):ToBeOk() + end) + end) + + ctx:Describe("Values", function() + ctx:Test("should give all values of table", function() + local tbl = { a = 1, b = 2, c = 3 } + local values = TableUtil.Values(tbl) + ctx:Expect(#values):ToBe(3) + ctx:Expect(table.find(values, 1)):ToBeOk() + ctx:Expect(table.find(values, 2)):ToBeOk() + ctx:Expect(table.find(values, 3)):ToBeOk() + end) + end) + + ctx:Describe("Find", function() + ctx:Test("should find item in array", function() + local tbl = { 10, 20, 30 } + local item, index = TableUtil.Find(tbl, function(value) + return (value == 20) + end) + ctx:Expect(item):ToBeOk() + ctx:Expect(index):ToBe(2) + ctx:Expect(item):ToBe(20) + end) + + ctx:Test("should find item in dictionary", function() + local tbl = { { Score = 10 }, { Score = 20 }, { Score = 30 } } + local item, index = TableUtil.Find(tbl, function(value) + return (value.Score == 20) + end) + ctx:Expect(item):ToBeOk() + ctx:Expect(index):ToBe(2) + ctx:Expect(item.Score):ToBe(20) + end) + end) + + ctx:Describe("Every", function() + ctx:Test("should see every value is above 20", function() + local tbl = { 21, 40, 200 } + local every = TableUtil.Every(tbl, function(n) + return (n > 20) + end) + ctx:Expect(every):ToBe(true) + end) + + ctx:Test("should see every value is not above 20", function() + local tbl = { 20, 40, 200 } + local every = TableUtil.Every(tbl, function(n) + return (n > 20) + end) + ctx:Expect(every):Not():ToBe(true) + end) + end) + + ctx:Describe("Some", function() + ctx:Test("should see some value is above 20", function() + local tbl = { 5, 40, 1 } + local every = TableUtil.Some(tbl, function(n) + return (n > 20) + end) + ctx:Expect(every):ToBe(true) + end) + + ctx:Test("should see some value is not above 20", function() + local tbl = { 5, 15, 1 } + local every = TableUtil.Some(tbl, function(n) + return (n > 20) + end) + ctx:Expect(every):Not():ToBe(true) + end) + end) + + ctx:Describe("Truncate", function() + ctx:Test("should truncate an array", function() + local t1 = { 1, 2, 3, 4, 5 } + local t2 = TableUtil.Truncate(t1, 3) + ctx:Expect(#t2):ToBe(3) + ctx:Expect(t2[1]):ToBe(t1[1]) + ctx:Expect(t2[2]):ToBe(t1[2]) + ctx:Expect(t2[3]):ToBe(t1[3]) + end) + + ctx:Test("should truncate an array with out of bounds sizes", function() + local t1 = { 1, 2, 3, 4, 5 } + ctx:Expect(function() + TableUtil.Truncate(t1, -1) + end) + :Not() + :ToThrow() + ctx:Expect(function() + TableUtil.Truncate(t1, #t1 + 1) + end) + :Not() + :ToThrow() + local t2 = TableUtil.Truncate(t1, #t1 + 10) + ctx:Expect(#t2):ToBe(#t1) + ctx:Expect(t2):Not():ToBe(t1) + end) + end) + + ctx:Describe("Lock", function() + ctx:Test("should lock a table", function() + local t = { abc = { xyz = { num = 32 } } } + ctx:Expect(function() + t.abc.xyz.num = 64 + end) + :Not() + :ToThrow() + local t2 = TableUtil.Lock(t) + ctx:Expect(t.abc.xyz.num):ToBe(64) + ctx:Expect(t):ToBe(t2) + ctx:Expect(function() + t.abc.xyz.num = 10 + end):ToThrow() + end) + end) + + ctx:Describe("Zip", function() + ctx:Test("should zip arrays together", function() + local t1 = { 1, 2, 3, 4, 5 } + local t2 = { 9, 8, 7, 6, 5 } + local t3 = { 1, 1, 1, 1, 1 } + local lastIndex = 0 + for i, v in TableUtil.Zip(t1, t2, t3) do + lastIndex = i + ctx:Expect(v[1]):ToBe(t1[i]) + ctx:Expect(v[2]):ToBe(t2[i]) + ctx:Expect(v[3]):ToBe(t3[i]) + end + ctx:Expect(lastIndex):ToBe(math.min(#t1, #t2, #t3)) + end) + + ctx:Test("should zip arrays of different lengths together", function() + local t1 = { 1, 2, 3, 4, 5 } + local t2 = { 9, 8, 7, 6 } + local t3 = { 1, 1, 1 } + local lastIndex = 0 + for i, v in TableUtil.Zip(t1, t2, t3) do + lastIndex = i + ctx:Expect(v[1]):ToBe(t1[i]) + ctx:Expect(v[2]):ToBe(t2[i]) + ctx:Expect(v[3]):ToBe(t3[i]) + end + ctx:Expect(lastIndex):ToBe(math.min(#t1, #t2, #t3)) + end) + + ctx:Test("should zip maps together", function() + local t1 = { a = 10, b = 20, c = 30 } + local t2 = { a = 100, b = 200, c = 300 } + local t3 = { a = 3000, b = 2000, c = 3000 } + for k, v in TableUtil.Zip(t1, t2, t3) do + ctx:Expect(v[1]):ToBe(t1[k]) + ctx:Expect(v[2]):ToBe(t2[k]) + ctx:Expect(v[3]):ToBe(t3[k]) + end + end) + + ctx:Test("should zip maps of different keys together", function() + local t1 = { a = 10, b = 20, c = 30, d = 40 } + local t2 = { a = 100, b = 200, c = 300, z = 10 } + local t3 = { a = 3000, b = 2000, c = 3000, x = 0 } + for k, v in TableUtil.Zip(t1, t2, t3) do + ctx:Expect(v[1]):ToBe(t1[k]) + ctx:Expect(v[2]):ToBe(t2[k]) + ctx:Expect(v[3]):ToBe(t3[k]) + end + end) + end) + + ctx:Describe("IsEmpty", function() + ctx:Test("should detect that table is empty", function() + local tbl = {} + local isEmpty = TableUtil.IsEmpty(tbl) + ctx:Expect(isEmpty):ToBe(true) + end) + + ctx:Test("should detect that array is not empty", function() + local tbl = { 10, 20, 30 } + local isEmpty = TableUtil.IsEmpty(tbl) + ctx:Expect(isEmpty):ToBe(false) + end) + + ctx:Test("should detect that dictionary is not empty", function() + local tbl = { a = 10, b = 20, c = 30 } + local isEmpty = TableUtil.IsEmpty(tbl) + ctx:Expect(isEmpty):ToBe(false) + end) + end) + + ctx:Describe("JSON", function() + ctx:Test("should encode json", function() + local tbl = { hello = "world" } + local json = TableUtil.EncodeJSON(tbl) + ctx:Expect(json):ToBe('{"hello":"world"}') + end) + + ctx:Test("should decode json", function() + local json = '{"hello":"world"}' + local tbl = TableUtil.DecodeJSON(json) + ctx:Expect(tbl):ToBeA("table") + ctx:Expect(tbl.hello):ToBe("world") + end) + end) +end diff --git a/modules/timer/init.spec.luau b/modules/timer/init.spec.luau deleted file mode 100644 index 892c0543..00000000 --- a/modules/timer/init.spec.luau +++ /dev/null @@ -1,69 +0,0 @@ -return function() - local Timer = require(script.Parent) - - describe("Timer", function() - local timer - - beforeEach(function() - timer = Timer.new(0.1) - timer.TimeFunction = os.clock - end) - - afterEach(function() - if timer then - timer:Destroy() - timer = nil - end - end) - - it("should create a new timer", function() - expect(Timer.Is(timer)).to.equal(true) - end) - - it("should tick appropriately", function() - local start = os.clock() - timer:Start() - timer.Tick:Wait() - local duration = (os.clock() - start) - expect(duration).to.be.near(duration, 0.02) - end) - - it("should start immediately", function() - local start = os.clock() - local stop = nil - timer.Tick:Connect(function() - if not stop then - stop = os.clock() - end - end) - timer:StartNow() - timer.Tick:Wait() - expect(stop).to.be.a("number") - local duration = (stop - start) - expect(duration).to.be.near(0, 0.02) - end) - - it("should stop", function() - local ticks = 0 - timer.Tick:Connect(function() - ticks += 1 - end) - timer:StartNow() - timer:Stop() - task.wait(1) - expect(ticks).to.equal(1) - end) - - it("should detect if running", function() - expect(timer:IsRunning()).to.equal(false) - timer:Start() - expect(timer:IsRunning()).to.equal(true) - timer:Stop() - expect(timer:IsRunning()).to.equal(false) - timer:StartNow() - expect(timer:IsRunning()).to.equal(true) - timer:Stop() - expect(timer:IsRunning()).to.equal(false) - end) - end) -end diff --git a/modules/timer/init.test.luau b/modules/timer/init.test.luau new file mode 100644 index 00000000..2f9f7446 --- /dev/null +++ b/modules/timer/init.test.luau @@ -0,0 +1,73 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local Timer = require(script.Parent) + + ctx:Describe("Timer", function() + local timer + + ctx:BeforeEach(function() + timer = Timer.new(0.1) + timer.TimeFunction = os.clock + end) + + ctx:AfterEach(function() + if timer then + timer:Destroy() + timer = nil + end + end) + + ctx:Test("should create a new timer", function() + ctx:Expect(Timer.Is(timer)):ToBe(true) + end) + + ctx:Test("should tick appropriately", function() + local start = os.clock() + timer:Start() + timer.Tick:Wait() + local duration = (os.clock() - start) + ctx:Expect(duration):ToBeNear(duration, 0.02) + end) + + ctx:Test("should start immediately", function() + local start = os.clock() + local stop = nil + timer.Tick:Connect(function() + if not stop then + stop = os.clock() + end + end) + timer:StartNow() + timer.Tick:Wait() + ctx:Expect(stop):ToBeA("number") + local duration = (stop - start) + ctx:Expect(duration):ToBeNear(0, 0.02) + end) + + ctx:Test("should stop", function() + local ticks = 0 + timer.Tick:Connect(function() + ticks += 1 + end) + timer:StartNow() + timer:Stop() + task.wait(1) + ctx:Expect(ticks):ToBe(1) + end) + + ctx:Test("should detect if running", function() + ctx:Expect(timer:IsRunning()):ToBe(false) + timer:Start() + ctx:Expect(timer:IsRunning()):ToBe(true) + timer:Stop() + ctx:Expect(timer:IsRunning()):ToBe(false) + timer:StartNow() + ctx:Expect(timer:IsRunning()):ToBe(true) + timer:Stop() + ctx:Expect(timer:IsRunning()):ToBe(false) + end) + end) +end diff --git a/modules/trove/init.luau b/modules/trove/init.luau index c178e585..3d325c71 100644 --- a/modules/trove/init.luau +++ b/modules/trove/init.luau @@ -12,6 +12,7 @@ export type Trove = { Add: (self: Trove, object: T & Trackable, cleanupMethod: string?) -> T, Remove: (self: Trove, object: T & Trackable) -> boolean, Clean: (self: Trove) -> (), + WrapClean: (self: Trove) -> () -> (), AttachToInstance: (self: Trove, instance: Instance) -> RBXScriptConnection, Destroy: (self: Trove) -> (), } diff --git a/modules/trove/init.spec.luau b/modules/trove/init.spec.luau deleted file mode 100644 index 1012428a..00000000 --- a/modules/trove/init.spec.luau +++ /dev/null @@ -1,196 +0,0 @@ -return function() - local Trove = require(script.Parent) - - describe("Trove", function() - local trove - - beforeEach(function() - trove = Trove.new() - end) - - afterEach(function() - if trove then - trove:Destroy() - trove = nil - end - end) - - it("should add and clean up roblox instance", function() - local part = Instance.new("Part") - part.Parent = workspace - trove:Add(part) - trove:Destroy() - expect(part.Parent).to.equal(nil) - end) - - it("should add and clean up roblox connection", function() - local connection = workspace.Changed:Connect(function() end) - trove:Add(connection) - trove:Destroy() - expect(connection.Connected).to.equal(false) - end) - - it("should add and clean up a table with a destroy method", function() - local tbl = { Destroyed = false } - function tbl:Destroy() - self.Destroyed = true - end - trove:Add(tbl) - trove:Destroy() - expect(tbl.Destroyed).to.equal(true) - end) - - it("should add and clean up a table with a disconnect method", function() - local tbl = { Connected = true } - function tbl:Disconnect() - self.Connected = false - end - trove:Add(tbl) - trove:Destroy() - expect(tbl.Connected).to.equal(false) - end) - - it("should add and clean up a function", function() - local fired = false - trove:Add(function() - fired = true - end) - trove:Destroy() - expect(fired).to.equal(true) - end) - - it("should allow a custom cleanup method", function() - local tbl = { Cleaned = false } - function tbl:Cleanup() - self.Cleaned = true - end - trove:Add(tbl, "Cleanup") - trove:Destroy() - expect(tbl.Cleaned).to.equal(true) - end) - - it("should return the object passed to add", function() - local part = Instance.new("Part") - local part2 = trove:Add(part) - expect(part).to.equal(part2) - trove:Destroy() - end) - - it("should fail to add object without proper cleanup method", function() - local tbl = {} - expect(function() - trove:Add(tbl) - end).to.throw() - end) - - it("should construct an object and add it", function() - local class = {} - class.__index = class - function class.new(msg) - local self = setmetatable({}, class) - self._msg = msg - self._destroyed = false - return self - end - function class:Destroy() - self._destroyed = true - end - local msg = "abc" - local obj = trove:Construct(class, msg) - expect(typeof(obj)).to.equal("table") - expect(getmetatable(obj)).to.equal(class) - expect(obj._msg).to.equal(msg) - expect(obj._destroyed).to.equal(false) - trove:Destroy() - expect(obj._destroyed).to.equal(true) - end) - - it("should connect to a signal", function() - local connection = trove:Connect(workspace.Changed, function() end) - expect(typeof(connection)).to.equal("RBXScriptConnection") - expect(connection.Connected).to.equal(true) - trove:Destroy() - expect(connection.Connected).to.equal(false) - end) - - it("should remove an object", function() - local connection = trove:Connect(workspace.Changed, function() end) - expect(trove:Remove(connection)).to.equal(true) - expect(connection.Connected).to.equal(false) - end) - - it("should not remove an object not in the trove", function() - local connection = workspace.Changed:Connect(function() end) - expect(trove:Remove(connection)).to.equal(false) - expect(connection.Connected).to.equal(true) - connection:Disconnect() - end) - - it("should attach to instance", function() - local part = Instance.new("Part") - part.Parent = workspace - local connection = trove:AttachToInstance(part) - expect(connection.Connected).to.equal(true) - part:Destroy() - expect(connection.Connected).to.equal(false) - end) - - it("should fail to attach to instance not in hierarchy", function() - local part = Instance.new("Part") - expect(function() - trove:AttachToInstance(part) - end).to.throw() - end) - - it("should extend itself", function() - local subTrove = trove:Extend() - local called = false - subTrove:Add(function() - called = true - end) - expect(subTrove).to.be.a("table") - expect(getmetatable(subTrove)).to.equal(Trove) - trove:Clean() - expect(called).to.equal(true) - end) - - it("should clone an instance", function() - local name = "TroveCloneTest" - local p1 = trove:Construct(Instance.new, "Part") - p1.Name = name - local p2 = trove:Clone(p1) - expect(typeof(p2)).to.equal("Instance") - expect(p2).to.never.equal(p1) - expect(p2.Name).to.equal(name) - expect(p1.Name).to.equal(p2.Name) - end) - - it("should clean up a thread", function() - local co = coroutine.create(function() end) - trove:Add(co) - expect(coroutine.status(co)).to.equal("suspended") - trove:Clean() - expect(coroutine.status(co)).to.equal("dead") - end) - - it("should not allow objects added during cleanup", function() - expect(function() - trove:Add(function() - trove:Add(function() end) - end) - trove:Clean() - end).to.throw() - end) - - it("should not allow objects to be removed during cleanup", function() - expect(function() - local f = function() end - trove:Add(f) - trove:Add(function() - trove:Remove(f) - end) - trove:Clean() - end).to.throw() - end) - end) -end diff --git a/modules/trove/init.test.luau b/modules/trove/init.test.luau new file mode 100644 index 00000000..a908855f --- /dev/null +++ b/modules/trove/init.test.luau @@ -0,0 +1,203 @@ +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) + local Trove = require(script.Parent) + + ctx:Describe("Trove", function() + local trove + + ctx:BeforeEach(function() + trove = Trove.new() + end) + + ctx:AfterEach(function() + if trove then + trove:Destroy() + trove = nil + end + end) + + ctx:Test("should add and clean up roblox instance", function() + local part = Instance.new("Part") + part.Parent = workspace + trove:Add(part) + trove:Destroy() + ctx:Expect(part.Parent):ToBeNil() + end) + + ctx:Test("should add and clean up roblox connection", function() + local connection = workspace.Changed:Connect(function() end) + trove:Add(connection) + trove:Destroy() + ctx:Expect(connection.Connected):ToBe(false) + end) + + ctx:Test("should add and clean up a table with a destroy method", function() + local tbl = { Destroyed = false } + function tbl:Destroy() + self.Destroyed = true + end + trove:Add(tbl) + trove:Destroy() + ctx:Expect(tbl.Destroyed):ToBe(true) + end) + + ctx:Test("should add and clean up a table with a disconnect method", function() + local tbl = { Connected = true } + function tbl:Disconnect() + self.Connected = false + end + trove:Add(tbl) + trove:Destroy() + ctx:Expect(tbl.Connected):ToBe(false) + end) + + ctx:Test("should add and clean up a function", function() + local fired = false + trove:Add(function() + fired = true + end) + trove:Destroy() + ctx:Expect(fired):ToBe(true) + end) + + ctx:Test("should allow a custom cleanup method", function() + local tbl = { Cleaned = false } + function tbl:Cleanup() + self.Cleaned = true + end + trove:Add(tbl, "Cleanup") + trove:Destroy() + ctx:Expect(tbl.Cleaned):ToBe(true) + end) + + ctx:Test("should return the object passed to add", function() + local part = Instance.new("Part") + local part2 = trove:Add(part) + ctx:Expect(part):ToBe(part2) + trove:Destroy() + end) + + ctx:Test("should fail to add object without proper cleanup method", function() + local tbl = {} + ctx:Expect(function() + trove:Add(tbl) + end):ToThrow() + end) + + ctx:Test("should construct an object and add it", function() + local class = {} + class.__index = class + function class.new(msg) + local self = setmetatable({}, class) + self._msg = msg + self._destroyed = false + return self + end + function class:Destroy() + self._destroyed = true + end + local msg = "abc" + local obj = trove:Construct(class, msg) + ctx:Expect(typeof(obj)):ToBe("table") + ctx:Expect(getmetatable(obj)):ToBe(class) + ctx:Expect(obj._msg):ToBe(msg) + ctx:Expect(obj._destroyed):ToBe(false) + trove:Destroy() + ctx:Expect(obj._destroyed):ToBe(true) + end) + + ctx:Test("should connect to a signal", function() + local connection = trove:Connect(workspace.Changed, function() end) + ctx:Expect(typeof(connection)):ToBe("RBXScriptConnection") + ctx:Expect(connection.Connected):ToBe(true) + trove:Destroy() + ctx:Expect(connection.Connected):ToBe(false) + end) + + ctx:Test("should remove an object", function() + local connection = trove:Connect(workspace.Changed, function() end) + ctx:Expect(trove:Remove(connection)):ToBe(true) + ctx:Expect(connection.Connected):ToBe(false) + end) + + ctx:Test("should not remove an object not in the trove", function() + local connection = workspace.Changed:Connect(function() end) + ctx:Expect(trove:Remove(connection)):ToBe(false) + ctx:Expect(connection.Connected):ToBe(true) + connection:Disconnect() + end) + + ctx:Test("should attach to instance", function() + local part = Instance.new("Part") + part.Parent = workspace + local connection = trove:AttachToInstance(part) + ctx:Expect(connection.Connected):ToBe(true) + part:Destroy() + ctx:Expect(connection.Connected):ToBe(false) + end) + + ctx:Test("should fail to attach to instance not in hierarchy", function() + local part = Instance.new("Part") + ctx:Expect(function() + trove:AttachToInstance(part) + end):ToThrow() + end) + + ctx:Test("should extend itself", function() + local subTrove = trove:Extend() + local called = false + subTrove:Add(function() + called = true + end) + ctx:Expect(typeof(subTrove)):ToBe("table") + ctx:Expect(getmetatable(subTrove)):ToBe(getmetatable(trove)) + trove:Clean() + ctx:Expect(called):ToBe(true) + end) + + ctx:Test("should clone an instance", function() + local name = "TroveCloneTest" + local p1 = trove:Construct(Instance.new, "Part") + p1.Name = name + local p2 = trove:Clone(p1) + ctx:Expect(typeof(p2)):ToBe("Instance") + ctx:Expect(p2):Not():ToBe(p1) + ctx:Expect(p2.Name):ToBe(name) + ctx:Expect(p1.Name):ToBe(p2.Name) + end) + + ctx:Test("should clean up a thread", function() + local co = coroutine.create(function() end) + trove:Add(co) + ctx:Expect(coroutine.status(co)):ToBe("suspended") + trove:Clean() + ctx:Expect(coroutine.status(co)):ToBe("dead") + end) + + ctx:Test("should not allow objects added during cleanup", function() + local added = false + trove:Add(function() + trove:Add(function() end) + added = true + end) + trove:Clean() + + ctx:Expect(added):ToBe(false) + end) + + ctx:Test("should not allow objects to be removed during cleanup", function() + local f = function() end + local removed = false + trove:Add(f) + trove:Add(function() + trove:Remove(f) + removed = true + end) + + ctx:Expect(removed):ToBe(false) + end) + end) +end diff --git a/modules/wait-for/init.spec.luau b/modules/wait-for/init.test.luau similarity index 56% rename from modules/wait-for/init.spec.luau rename to modules/wait-for/init.test.luau index 91de2568..ed0ce591 100644 --- a/modules/wait-for/init.spec.luau +++ b/modules/wait-for/init.test.luau @@ -1,6 +1,10 @@ -return function() - local WaitFor = require(script.Parent) +local ServerScriptService = game:GetService("ServerScriptService") + +local Test = require(ServerScriptService.TestRunner.Test) + +return function(ctx: Test.TestContext) local Promise = require(script.Parent.Parent.Promise) + local WaitFor = require(script.Parent) local instances = {} @@ -12,30 +16,30 @@ return function() return instance end - afterEach(function() - for _, inst in ipairs(instances) do - task.delay(0, function() - inst:Destroy() - end) - end - table.clear(instances) - end) + ctx:Describe("WaitFor", function() + ctx:AfterEach(function() + for _, inst in ipairs(instances) do + task.delay(0, function() + inst:Destroy() + end) + end + table.clear(instances) + end) - describe("WaitFor", function() - it("should wait for child", function() + ctx:Test("should wait for child", function() local parent = workspace local childName = "TestChild" task.delay(0.1, Create, childName, parent) local success, instance = WaitFor.Child(parent, childName):await() - expect(success).to.equal(true) - expect(typeof(instance)).to.equal("Instance") - expect(instance.Name).to.equal(childName) - expect(instance.Parent).to.equal(parent) + ctx:Expect(success):ToBe(true) + ctx:Expect(typeof(instance)):ToBe("Instance") + ctx:Expect(instance.Name):ToBe(childName) + ctx:Expect(instance.Parent):ToBe(parent) end) - it("should stop waiting for child if parent is unparented", function() + ctx:Test("should stop waiting for child if parent is unparented", function() local parent = Create("SomeParent", workspace) local childName = "TestChild" @@ -44,17 +48,17 @@ return function() end) local success, err = WaitFor.Child(parent, childName):await() - expect(success).to.equal(false) - expect(err).to.equal(WaitFor.Error.Unparented) + ctx:Expect(success):ToBe(false) + ctx:Expect(err):ToBe(WaitFor.Error.Unparented) end) - it("should stop waiting for child if timeout is reached", function() + ctx:Test("should stop waiting for child if timeout is reached", function() local success, err = WaitFor.Child(workspace, "InstanceThatDoesNotExist", 0.1):await() - expect(success).to.equal(false) - expect(Promise.Error.isKind(err, Promise.Error.Kind.TimedOut)).to.equal(true) + ctx:Expect(success):ToBe(false) + ctx:Expect(Promise.Error.isKind(err, Promise.Error.Kind.TimedOut)):ToBe(true) end) - it("should wait for children", function() + ctx:Test("should wait for children", function() local parent = workspace local childrenNames = { "TestChild01", "TestChild02", "TestChild03" } @@ -63,15 +67,15 @@ return function() task.delay(0.05, Create, childrenNames[3], parent) local success, children = WaitFor.Children(parent, childrenNames):await() - expect(success).to.equal(true) + ctx:Expect(success):ToBe(true) for i, child in ipairs(children) do - expect(typeof(child)).to.equal("Instance") - expect(child.Name).to.equal(childrenNames[i]) - expect(child.Parent).to.equal(parent) + ctx:Expect(typeof(child)):ToBe("Instance") + ctx:Expect(child.Name):ToBe(childrenNames[i]) + ctx:Expect(child.Parent):ToBe(parent) end end) - it("should fail if any children are no longer parented in parent", function() + ctx:Test("should fail if any children are no longer parented in parent", function() local parent = workspace local childrenNames = { "TestChild04", "TestChild05", "TestChild06" } @@ -87,24 +91,24 @@ return function() end) local success, err = WaitFor.Children(parent, childrenNames):await() - expect(success).to.equal(false) - expect(err).to.equal(WaitFor.Error.ParentChanged) + ctx:Expect(success):ToBe(false) + ctx:Expect(err):ToBe(WaitFor.Error.ParentChanged) end) - it("should wait for descendant", function() + ctx:Test("should wait for descendant", function() local parent = workspace local descendantName = "TestDescendant" task.delay(0.1, Create, descendantName, Create("TestFolder", parent)) local success, descendant = WaitFor.Descendant(parent, descendantName):await() - expect(success).to.equal(true) - expect(typeof(descendant)).to.equal("Instance") - expect(descendant.Name).to.equal(descendantName) - expect(descendant:IsDescendantOf(parent)).to.equal(true) + ctx:Expect(success):ToBe(true) + ctx:Expect(typeof(descendant)):ToBe("Instance") + ctx:Expect(descendant.Name):ToBe(descendantName) + ctx:Expect(descendant:IsDescendantOf(parent)):ToBe(true) end) - it("should wait for many descendants", function() + ctx:Test("should wait for many descendants", function() local parent = workspace local descendantNames = { "TestDescendant01", "TestDescendant02", "TestDescendant03" } @@ -113,15 +117,15 @@ return function() task.delay(0.2, Create, descendantNames[3], Create("TestFolder4", Create("TestFolder3", parent))) local success, descendants = WaitFor.Descendants(parent, descendantNames):await() - expect(success).to.equal(true) + ctx:Expect(success):ToBe(true) for i, descendant in ipairs(descendants) do - expect(typeof(descendant)).to.equal("Instance") - expect(descendant.Name == descendantNames[i]).to.equal(true) - expect(descendant:IsDescendantOf(parent)).to.equal(true) + ctx:Expect(typeof(descendant)):ToBe("Instance") + ctx:Expect(descendant.Name == descendantNames[i]):ToBe(true) + ctx:Expect(descendant:IsDescendantOf(parent)):ToBe(true) end end) - it("should wait for primarypart", function() + ctx:Test("should wait for primarypart", function() local model = Instance.new("Model") local part = Instance.new("Part") part.Anchored = true @@ -134,15 +138,15 @@ return function() end) local success, primary = WaitFor.PrimaryPart(model):await() - expect(success).to.equal(true) - expect(typeof(primary)).to.equal("Instance") - expect(primary).to.equal(part) - expect(model.PrimaryPart).to.equal(primary) + ctx:Expect(success):ToBe(true) + ctx:Expect(typeof(primary)):ToBe("Instance") + ctx:Expect(primary):ToBe(part) + ctx:Expect(model.PrimaryPart):ToBe(primary) model:Destroy() end) - it("should wait for objectvalue", function() + ctx:Test("should wait for objectvalue", function() local objValue = Instance.new("ObjectValue") objValue.Parent = workspace @@ -153,15 +157,15 @@ return function() end) local success, value = WaitFor.ObjectValue(objValue):await() - expect(success).to.equal(true) - expect(typeof(value)).to.equal("Instance") - expect(value).to.equal(instance) - expect(objValue.Value == value) + ctx:Expect(success):ToBe(true) + ctx:Expect(typeof(value)):ToBe("Instance") + ctx:Expect(value):ToBe(instance) + ctx:Expect(objValue.Value == value) objValue:Destroy() end) - it("should wait for custom predicate", function() + ctx:Test("should wait for custom predicate", function() local instance task.delay(0.1, function() instance = Create("CustomInstance", workspace) @@ -170,9 +174,9 @@ return function() local success, inst = WaitFor.Custom(function() return instance end):await() - expect(success).to.equal(true) - expect(typeof(inst)).to.equal("Instance") - expect(inst).to.equal(instance) + ctx:Expect(success):ToBe(true) + ctx:Expect(typeof(inst)):ToBe("Instance") + ctx:Expect(inst):ToBe(instance) end) end) end diff --git a/requirements.txt b/requirements.txt index e59495ee..71c2d611 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ watchdog +requests diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 00000000..820ff424 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,108 @@ +import argparse +import requests +import json +import os +import time + + +# Seconds between status polling +POLL_STATUS_INTERVAL = 2 + +ROOT_API = "https://apis.roblox.com/cloud/v2" +RUNNER_SCRIPT = "ci/RunTests.luau" + + +def run_script(script_path: str, api_key: str, universe_id: int, place_id: int): + with open(script_path, "r") as script: + script_source = script.read() + + data = { + "script": script_source, + } + headers = { + "Content-Type": "application/json", + "x-api-key": api_key, + } + + run_script_url = f"{ROOT_API}/universes/{universe_id}/places/{place_id}/luau-execution-session-tasks" + + res = requests.post(run_script_url, data=json.dumps(data), headers=headers) + res.raise_for_status() + res_json = res.json() + + get_status_url = f"{ROOT_API}/{res_json['path']}?view=BASIC" # view can be BASIC or FULL + + return get_status_url + + +def get_script_status(get_status_url: str, api_key: str): + res = requests.get(get_status_url, headers={"x-api-key": api_key}) + res.raise_for_status() + res_json = res.json() + + return res_json + + +def await_script_completion(get_status_url: str, api_key: str, timeout: float): + start = time.time() + + last_state = "" + data = None + while True: + data = get_script_status(get_status_url, api_key) + + state = data["state"] + if state != last_state: + print(f"State changed: {state}") + last_state = state + else: + print(f"{state}...") + + if state == "COMPLETE" or state == "FAILED" or state == "CANCELLED": + break + + # Timeout condition: + if time.time() - start > timeout: + print("timeout") + exit(1) + + time.sleep(POLL_STATUS_INTERVAL) + + return data + + +def run_tests(): + parser = argparse.ArgumentParser("RunTests") + parser.add_argument("uid", help="Universe ID", type=int) + parser.add_argument("pid", help="Place ID", type=int) + args = parser.parse_args() + + api_key = os.getenv("API_KEY") + + get_status_url = run_script(RUNNER_SCRIPT, api_key, args.uid, args.pid) + + data = await_script_completion(get_status_url, api_key, 60) + + match data["state"]: + case "COMPLETE": + result = data["output"]["results"][0] + all_pass = result["AllPass"] + + print(result["Output"]) + + if not all_pass: + exit(1) + + case "FAILED": + print(data["error"]) + exit(1) + + case "CANCELLED": + print("Cancelled") + exit(1) + + print("All tests passed") + + +if __name__ == "__main__": + run_tests() diff --git a/selene.toml b/selene.toml index bc37897a..c4ddb460 100644 --- a/selene.toml +++ b/selene.toml @@ -1 +1 @@ -std = "roblox+testez+future" +std = "roblox" diff --git a/test.project.json b/test.project.json index 35598fda..d30d97ae 100644 --- a/test.project.json +++ b/test.project.json @@ -4,7 +4,7 @@ "$className": "DataModel", "ReplicatedStorage": { "$className": "ReplicatedStorage", - "Test": { + "Modules": { "$path": "test" } }, diff --git a/test/wally.lock b/test/wally.lock index fa176b94..346cd099 100644 --- a/test/wally.lock +++ b/test/wally.lock @@ -2,12 +2,7 @@ # It is not intended for manual editing. registry = "test" -[[package]] -name = "roblox/testez" -version = "0.4.1" -dependencies = [] - [[package]] name = "sleit/rbxutil" version = "0.1.0" -dependencies = [["TestEZ", "roblox/testez@0.4.1"]] +dependencies = [] diff --git a/test/wally.toml b/test/wally.toml index 825326d4..2d14a754 100644 --- a/test/wally.toml +++ b/test/wally.toml @@ -3,6 +3,3 @@ name = "sleit/rbxutil" version = "0.1.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" - -[dependencies] -TestEZ = "roblox/testez@^0.4.1" diff --git a/testez.toml b/testez.toml deleted file mode 100644 index c2c5a984..00000000 --- a/testez.toml +++ /dev/null @@ -1,66 +0,0 @@ -[[afterAll.args]] -type = "function" - -[[afterEach.args]] -type = "function" - -[[beforeAll.args]] -type = "function" - -[[beforeEach.args]] -type = "function" - -[[describe.args]] -type = "string" - -[[describe.args]] -type = "function" - -[[describeFOCUS.args]] -type = "string" - -[[describeFOCUS.args]] -type = "function" - -[[describeSKIP.args]] -type = "string" - -[[describeSKIP.args]] -type = "function" - -[[expect.args]] -type = "any" - -[[FIXME.args]] -type = "string" -required = false - -[FOCUS] -args = [] - -[[it.args]] -type = "string" - -[[it.args]] -type = "function" - -[[itFIXME.args]] -type = "string" - -[[itFIXME.args]] -type = "function" - -[[itFOCUS.args]] -type = "string" - -[[itFOCUS.args]] -type = "function" - -[[itSKIP.args]] -type = "string" - -[[itSKIP.args]] -type = "function" - -[SKIP] -args = []