Skip to content

Commit 7a69ebf

Browse files
authored
Merge pull request #728 from Quenty/users/quenty/octree-unit-tests
test(octree): Add unit tests for Octree, OctreeNode, and OctreeRegionUtils
2 parents d577f15 + 938a8e2 commit 7a69ebf

9 files changed

Lines changed: 812 additions & 1 deletion

File tree

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/octree/deploy.nevermore.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"targets": {
3+
"test": {
4+
"universeId": 9716264427,
5+
"placeId": 97463009412110,
6+
"project": "test/default.project.json",
7+
"scriptTemplate": "test/scripts/Server/ServerMain.server.lua"
8+
}
9+
}
10+
}

src/octree/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
],
3131
"dependencies": {
3232
"@quenty/draw": "workspace:*",
33-
"@quenty/loader": "workspace:*"
33+
"@quenty/loader": "workspace:*",
34+
"@quenty/nevermore-test-runner": "workspace:*",
35+
"@quentystudios/jest-lua": "3.10.0-quenty.2"
3436
},
3537
"publishConfig": {
3638
"access": "public"
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
--!nonstrict
2+
--[[
3+
@class Octree.spec.lua
4+
]]
5+
6+
local require = (require :: any)(
7+
game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent
8+
).bootstrapStory(script) :: typeof(require(script.Parent.loader).load(script))
9+
10+
local Jest = require("Jest")
11+
local Octree = require("Octree")
12+
13+
local describe = Jest.Globals.describe
14+
local expect = Jest.Globals.expect
15+
local it = Jest.Globals.it
16+
17+
-- Collects the stored objects out of a { OctreeNode } list for order-independent asserts
18+
local function objectSet(nodes)
19+
local set = {}
20+
for _, node in nodes do
21+
set[node:GetObject()] = true
22+
end
23+
return set
24+
end
25+
26+
describe("Octree.new", function()
27+
it("constructs an empty octree", function()
28+
local octree = Octree.new()
29+
30+
expect(octree.ClassName).toBe("Octree")
31+
expect(#octree:GetAllNodes()).toBe(0)
32+
end)
33+
34+
it("returns no results when searching an empty octree", function()
35+
local octree = Octree.new()
36+
37+
local objects, distances = octree:RadiusSearch(Vector3.zero, 100)
38+
expect(#objects).toBe(0)
39+
expect(#distances).toBe(0)
40+
end)
41+
end)
42+
43+
describe("Octree.CreateNode", function()
44+
it("stores a node retrievable via search", function()
45+
local octree = Octree.new()
46+
octree:CreateNode(Vector3.zero, "A")
47+
48+
local objects = octree:RadiusSearch(Vector3.zero, 10)
49+
expect(#objects).toBe(1)
50+
expect(objects[1]).toBe("A")
51+
end)
52+
53+
it("supports multiple nodes at the same position", function()
54+
local octree = Octree.new()
55+
octree:CreateNode(Vector3.zero, "A")
56+
octree:CreateNode(Vector3.zero, "B")
57+
58+
local objects = octree:RadiusSearch(Vector3.zero, 10)
59+
expect(#objects).toBe(2)
60+
61+
local set = {}
62+
for _, object in objects do
63+
set[object] = true
64+
end
65+
expect(set.A).toBe(true)
66+
expect(set.B).toBe(true)
67+
end)
68+
69+
it("supports arbitrary object types including tables and instances", function()
70+
local octree = Octree.new()
71+
local tableObject = {}
72+
local instanceObject = Instance.new("Folder")
73+
74+
octree:CreateNode(Vector3.zero, tableObject)
75+
octree:CreateNode(Vector3.zero, instanceObject)
76+
77+
local set = objectSet(octree:GetAllNodes())
78+
expect(set[tableObject]).toBe(true)
79+
expect(set[instanceObject]).toBe(true)
80+
81+
instanceObject:Destroy()
82+
end)
83+
84+
it("rejects a non-Vector3 position", function()
85+
local octree = Octree.new()
86+
expect(function()
87+
octree:CreateNode(5 :: any, "A")
88+
end).toThrow()
89+
end)
90+
91+
it("rejects a nil object", function()
92+
local octree = Octree.new()
93+
expect(function()
94+
octree:CreateNode(Vector3.zero, nil :: any)
95+
end).toThrow()
96+
end)
97+
end)
98+
99+
describe("Octree.GetAllNodes", function()
100+
it("returns every created node regardless of region", function()
101+
local octree = Octree.new()
102+
octree:CreateNode(Vector3.zero, "A")
103+
octree:CreateNode(Vector3.new(1000, 0, 0), "B")
104+
octree:CreateNode(Vector3.new(0, -2000, 0), "C")
105+
106+
local set = objectSet(octree:GetAllNodes())
107+
expect(set.A).toBe(true)
108+
expect(set.B).toBe(true)
109+
expect(set.C).toBe(true)
110+
expect(#octree:GetAllNodes()).toBe(3)
111+
end)
112+
113+
it("stops returning a node after it is destroyed", function()
114+
local octree = Octree.new()
115+
local node = octree:CreateNode(Vector3.zero, "A")
116+
expect(#octree:GetAllNodes()).toBe(1)
117+
118+
node:Destroy()
119+
expect(#octree:GetAllNodes()).toBe(0)
120+
end)
121+
end)
122+
123+
describe("Octree.RadiusSearch", function()
124+
it("includes nodes inside the radius and excludes those outside", function()
125+
local octree = Octree.new()
126+
octree:CreateNode(Vector3.zero, "A")
127+
octree:CreateNode(Vector3.new(0, 0, 5), "B")
128+
octree:CreateNode(Vector3.new(0, 0, 1000), "C")
129+
130+
local objects = octree:RadiusSearch(Vector3.zero, 100)
131+
local set = {}
132+
for _, object in objects do
133+
set[object] = true
134+
end
135+
expect(set.A).toBe(true)
136+
expect(set.B).toBe(true)
137+
expect(set.C).never.toBe(true)
138+
end)
139+
140+
it("returns the squared distance for each object", function()
141+
local octree = Octree.new()
142+
octree:CreateNode(Vector3.new(0, 0, 5), "B")
143+
144+
local objects, distances = octree:RadiusSearch(Vector3.zero, 100)
145+
expect(#objects).toBe(1)
146+
expect(distances[1]).toBeCloseTo(25, 6)
147+
end)
148+
149+
it("excludes a node just outside the radius", function()
150+
local octree = Octree.new()
151+
octree:CreateNode(Vector3.new(0, 0, 5), "B")
152+
153+
-- Node is exactly 5 studs away; a radius of 4 must not include it
154+
local objects = octree:RadiusSearch(Vector3.zero, 4)
155+
expect(#objects).toBe(0)
156+
end)
157+
158+
it("finds a node at its exact position with radius zero", function()
159+
local octree = Octree.new()
160+
octree:CreateNode(Vector3.new(10, 20, 30), "A")
161+
162+
local objects, distances = octree:RadiusSearch(Vector3.new(10, 20, 30), 0)
163+
expect(#objects).toBe(1)
164+
expect(distances[1]).toBeCloseTo(0, 6)
165+
end)
166+
167+
it("works across negative coordinates and separate top-level regions", function()
168+
local octree = Octree.new()
169+
octree:CreateNode(Vector3.new(-1000, -1000, -1000), "neg")
170+
171+
local objects = octree:RadiusSearch(Vector3.new(-1000, -1000, -1000), 10)
172+
expect(#objects).toBe(1)
173+
expect(objects[1]).toBe("neg")
174+
end)
175+
176+
it("rejects a non-Vector3 position", function()
177+
local octree = Octree.new()
178+
expect(function()
179+
octree:RadiusSearch(5 :: any, 10)
180+
end).toThrow()
181+
end)
182+
183+
it("rejects a non-number radius", function()
184+
local octree = Octree.new()
185+
expect(function()
186+
octree:RadiusSearch(Vector3.zero, "big" :: any)
187+
end).toThrow()
188+
end)
189+
end)
190+
191+
describe("Octree.KNearestNeighborsSearch", function()
192+
it("returns the k closest objects ordered by distance", function()
193+
local octree = Octree.new()
194+
octree:CreateNode(Vector3.zero, "A")
195+
octree:CreateNode(Vector3.new(0, 0, 5), "B")
196+
octree:CreateNode(Vector3.new(0, 0, 10), "C")
197+
198+
local objects, distances = octree:KNearestNeighborsSearch(Vector3.zero, 2, 100)
199+
expect(#objects).toBe(2)
200+
expect(objects[1]).toBe("A")
201+
expect(objects[2]).toBe("B")
202+
expect(distances[1]).toBeCloseTo(0, 6)
203+
expect(distances[2]).toBeCloseTo(25, 6)
204+
end)
205+
206+
it("returns everything in range when k exceeds the node count", function()
207+
local octree = Octree.new()
208+
octree:CreateNode(Vector3.zero, "A")
209+
octree:CreateNode(Vector3.new(0, 0, 5), "B")
210+
211+
local objects = octree:KNearestNeighborsSearch(Vector3.zero, 10, 100)
212+
expect(#objects).toBe(2)
213+
end)
214+
215+
it("returns nothing when k is zero", function()
216+
local octree = Octree.new()
217+
octree:CreateNode(Vector3.zero, "A")
218+
219+
local objects, distances = octree:KNearestNeighborsSearch(Vector3.zero, 0, 100)
220+
expect(#objects).toBe(0)
221+
expect(#distances).toBe(0)
222+
end)
223+
224+
it("respects the radius bound", function()
225+
local octree = Octree.new()
226+
octree:CreateNode(Vector3.zero, "A")
227+
octree:CreateNode(Vector3.new(0, 0, 50), "far")
228+
229+
local objects = octree:KNearestNeighborsSearch(Vector3.zero, 5, 10)
230+
expect(#objects).toBe(1)
231+
expect(objects[1]).toBe("A")
232+
end)
233+
end)

0 commit comments

Comments
 (0)