]> git.lizzy.rs Git - worldedit.git/blob - worldedit/test.lua
Fix deserialization of schematics with node names table
[worldedit.git] / worldedit / test.lua
1 ---------------------
2 -- Helpers
3 ---------------------
4
5 local vec = vector.new
6 local vecw = function(axis, n, base)
7         local ret = vec(base)
8         ret[axis] = n
9         return ret
10 end
11 local pos2str = minetest.pos_to_string
12 local get_node = minetest.get_node
13 local set_node = minetest.set_node
14
15 ---------------------
16 -- Nodes
17 ---------------------
18 local air = "air"
19 local testnode1
20 local testnode2
21 local testnode3
22 -- Loads nodenames to use for tests
23 local function init_nodes()
24         testnode1 = minetest.registered_aliases["mapgen_stone"]
25         testnode2 = minetest.registered_aliases["mapgen_dirt"]
26         testnode3 = minetest.registered_aliases["mapgen_cobble"] or minetest.registered_aliases["mapgen_dirt_with_grass"]
27         assert(testnode1 and testnode2 and testnode3)
28 end
29 -- Writes repeating pattern into given area
30 local function place_pattern(pos1, pos2, pattern)
31         local pos = vec()
32         local node = {name=""}
33         local i = 1
34         for z = pos1.z, pos2.z do
35                 pos.z = z
36         for y = pos1.y, pos2.y do
37                 pos.y = y
38         for x = pos1.x, pos2.x do
39                 pos.x = x
40                 node.name = pattern[i]
41                 set_node(pos, node)
42                 i = i % #pattern + 1
43         end
44         end
45         end
46 end
47
48
49 ---------------------
50 -- Area management
51 ---------------------
52 assert(minetest.get_mapgen_setting("mg_name") == "singlenode")
53 local area = {}
54 do
55         local areamin, areamax
56         local off
57         local c_air = minetest.get_content_id(air)
58         local vbuffer = {}
59         -- Assign a new area for use, will emerge and then call ready()
60         area.assign = function(min, max, ready)
61                 areamin = min
62                 areamax = max
63                 minetest.emerge_area(min, max, function(bpos, action, remaining)
64                         assert(action ~= minetest.EMERGE_ERRORED)
65                         if remaining > 0 then return end
66                         minetest.after(0, function()
67                                 area.clear()
68                                 ready()
69                         end)
70                 end)
71         end
72         -- Reset area contents and state
73         area.clear = function()
74                 local vmanip = minetest.get_voxel_manip(areamin, areamax)
75                 local vpos1, vpos2 = vmanip:get_emerged_area()
76                 local vcount = (vpos2.x - vpos1.x + 1) * (vpos2.y - vpos1.y + 1) * (vpos2.z - vpos1.z + 1)
77                 if #vbuffer ~= vcount then
78                         vbuffer = {}
79                         for i = 1, vcount do
80                                 vbuffer[i] = c_air
81                         end
82                 end
83                 vmanip:set_data(vbuffer)
84                 vmanip:write_to_map()
85                 off = vec(0, 0, 0)
86         end
87         -- Returns an usable area [pos1, pos2] that does not overlap previous ones
88         area.get = function(sizex, sizey, sizez)
89                 local size
90                 if sizey == nil or sizez == nil then
91                         size = {x=sizex, y=sizex, z=sizex}
92                 else
93                         size = {x=sizex, y=sizey, z=sizez}
94                 end
95                 local pos1 = vector.add(areamin, off)
96                 local pos2 = vector.subtract(vector.add(pos1, size), 1)
97                 if pos2.x > areamax.x or pos2.y > areamax.y or pos2.z > areamax.z then
98                         error("Internal failure: out of space")
99                 end
100                 off = vector.add(off, size)
101                 return pos1, pos2
102         end
103         -- Returns an axis and count (= n) relative to the last-requested area that is unoccupied
104         area.dir = function(n)
105                 local pos1 = vector.add(areamin, off)
106                 if pos1.x + n <= areamax.x then
107                         off.x = off.x + n
108                         return "x", n
109                 elseif pos1.x + n <= areamax.y then
110                         off.y = off.y + n
111                         return "y", n
112                 elseif pos1.z + n <= areamax.z then
113                         off.z = off.z + n
114                         return "z", n
115                 end
116                 error("Internal failure: out of space")
117         end
118         -- Returns [XYZ] margin (list of pos pairs) of n around last-requested area
119         -- (may actually be larger but doesn't matter)
120         area.margin = function(n)
121                 local pos1, pos2 = area.get(n)
122                 return {
123                         { vec(areamin.x, areamin.y, pos1.z), pos2 }, -- X/Y
124                         { vec(areamin.x, pos1.y, areamin.z), pos2 }, -- X/Z
125                         { vec(pos1.x, areamin.y, areamin.z), pos2 }, -- Y/Z
126                 }
127         end
128 end
129 -- Split an existing area into two non-overlapping [pos1, half1], [half2, pos2] parts; returns half1, half2
130 area.split = function(pos1, pos2)
131         local axis
132         if pos2.x - pos1.x >= 1 then
133                 axis = "x"
134         elseif pos2.y - pos1.y >= 1 then
135                 axis = "y"
136         elseif pos2.z - pos1.z >= 1 then
137                 axis = "z"
138         else
139                 error("Internal failure: area too small to split")
140         end
141         local hspan = math.floor((pos2[axis] - pos1[axis] + 1) / 2)
142         local half1 = vecw(axis, pos1[axis] + hspan - 1, pos2)
143         local half2 = vecw(axis, pos1[axis] + hspan, pos2)
144         return half1, half2
145 end
146
147
148 ---------------------
149 -- Checks
150 ---------------------
151 local check = {}
152 -- Check that all nodes in [pos1, pos2] are the node(s) specified
153 check.filled = function(pos1, pos2, nodes)
154         if type(nodes) == "string" then
155                 nodes = { nodes }
156         end
157         local _, counts = minetest.find_nodes_in_area(pos1, pos2, nodes)
158         local total = worldedit.volume(pos1, pos2)
159         local sum = 0
160         for _, n in pairs(counts) do
161                 sum = sum + n
162         end
163         if sum ~= total then
164                 error((total - sum) .. " " .. table.concat(nodes, ",") .. " nodes missing in " ..
165                         pos2str(pos1) .. " -> " .. pos2str(pos2))
166         end
167 end
168 -- Check that none of the nodes in [pos1, pos2] are the node(s) specified
169 check.not_filled = function(pos1, pos2, nodes)
170         if type(nodes) == "string" then
171                 nodes = { nodes }
172         end
173         local _, counts = minetest.find_nodes_in_area(pos1, pos2, nodes)
174         for nodename, n in pairs(counts) do
175                 if n ~= 0 then
176                         error(counts[nodename] .. " " .. nodename .. " nodes found in " ..
177                                 pos2str(pos1) .. " -> " .. pos2str(pos2))
178                 end
179         end
180 end
181 -- Check that all of the areas are only made of node(s) specified
182 check.filled2 = function(list, nodes)
183         for _, pos in ipairs(list) do
184                 check.filled(pos[1], pos[2], nodes)
185         end
186 end
187 -- Check that none of the areas contain the node(s) specified
188 check.not_filled2 = function(list, nodes)
189         for _, pos in ipairs(list) do
190                 check.not_filled(pos[1], pos[2], nodes)
191         end
192 end
193 -- Checks presence of a repeating pattern in [pos1, po2] (cf. place_pattern)
194 check.pattern = function(pos1, pos2, pattern)
195         local pos = vec()
196         local i = 1
197         for z = pos1.z, pos2.z do
198                 pos.z = z
199         for y = pos1.y, pos2.y do
200                 pos.y = y
201         for x = pos1.x, pos2.x do
202                 pos.x = x
203                 local node = get_node(pos)
204                 if node.name ~= pattern[i] then
205                         error(pattern[i] .. " not found at " .. pos2str(pos) .. " (i=" .. i .. ")")
206                 end
207                 i = i % #pattern + 1
208         end
209         end
210         end
211 end
212
213
214 ---------------------
215 -- The actual tests
216 ---------------------
217 local tests = {}
218 local function register_test(name, func, opts)
219         assert(type(name) == "string")
220         assert(func == nil or type(func) == "function")
221         if not opts then
222                 opts = {}
223         else
224                 opts = table.copy(opts)
225         end
226         opts.name = name
227         opts.func = func
228         table.insert(tests, opts)
229 end
230 -- How this works:
231 --   register_test registers a test with a name and function
232 --   The function should return if the test passes or otherwise cause a Lua error
233 --   The basic structure is: get areas + do operations + check results
234 -- Helpers:
235 --   area.get must be used to retrieve areas that can be operated on (these will be cleared before each test)
236 --   check.filled / check.not_filled can be used to check the result
237 --   area.margin + check.filled2 is useful to make sure nodes weren't placed too far
238 --   place_pattern + check.pattern is useful to test ops that operate on existing data
239
240
241 register_test("Internal self-test")
242 register_test("is area loaded?", function()
243         local pos1, _ = area.get(1)
244         assert(get_node(pos1).name == "air")
245 end, {dry=true})
246
247 register_test("area.split", function()
248         for i = 2, 6 do
249                 local pos1, pos2 = area.get(1, 1, i)
250                 local half1, half2 = area.split(pos1, pos2)
251                 assert(pos1.x == half1.x and pos1.y == half1.y)
252                 assert(half1.x == half2.x and half1.y == half2.y)
253                 assert(half1.z + 1 == half2.z)
254                 if i % 2 == 0 then
255                         assert((half1.z - pos1.z) == (pos2.z - half2.z)) -- divided equally
256                 end
257         end
258 end, {dry=true})
259
260 register_test("check.filled", function()
261         local pos1, pos2 = area.get(1, 2, 1)
262         set_node(pos1, {name=testnode1})
263         set_node(pos2, {name=testnode2})
264         check.filled(pos1, pos1, testnode1)
265         check.filled(pos1, pos2, {testnode1, testnode2})
266         check.not_filled(pos1, pos1, air)
267         check.not_filled(pos1, pos2, {air, testnode3})
268 end)
269
270 register_test("pattern", function()
271         local pos1, pos2 = area.get(3, 2, 1)
272         local pattern = {testnode1, testnode3}
273         place_pattern(pos1, pos2, pattern)
274         assert(get_node(pos1).name == testnode1)
275         check.pattern(pos1, pos2, pattern)
276 end)
277
278
279 register_test("Generic node manipulations")
280 register_test("worldedit.set", function()
281         local pos1, pos2 = area.get(10)
282         local m = area.margin(1)
283
284         worldedit.set(pos1, pos2, testnode1)
285
286         check.filled(pos1, pos2, testnode1)
287         check.filled2(m, air)
288 end)
289
290 register_test("worldedit.set mix", function()
291         local pos1, pos2 = area.get(10)
292         local m = area.margin(1)
293
294         worldedit.set(pos1, pos2, {testnode1, testnode2})
295
296         check.filled(pos1, pos2, {testnode1, testnode2})
297         check.filled2(m, air)
298 end)
299
300 register_test("worldedit.replace", function()
301         local pos1, pos2 = area.get(10)
302         local half1, half2 = area.split(pos1, pos2)
303
304         worldedit.set(pos1, half1, testnode1)
305         worldedit.set(half2, pos2, testnode2)
306         worldedit.replace(pos1, pos2, testnode1, testnode3)
307
308         check.not_filled(pos1, pos2, testnode1)
309         check.filled(pos1, half1, testnode3)
310         check.filled(half2, pos2, testnode2)
311 end)
312
313 register_test("worldedit.replace inverse", function()
314         local pos1, pos2 = area.get(10)
315         local half1, half2 = area.split(pos1, pos2)
316
317         worldedit.set(pos1, half1, testnode1)
318         worldedit.set(half2, pos2, testnode2)
319         worldedit.replace(pos1, pos2, testnode1, testnode3, true)
320
321         check.filled(pos1, half1, testnode1)
322         check.filled(half2, pos2, testnode3)
323 end)
324
325 -- FIXME?: this one looks overcomplicated
326 register_test("worldedit.copy", function()
327         local pos1, pos2 = area.get(4)
328         local axis, n = area.dir(2)
329         local m = area.margin(1)
330         local b = pos1[axis]
331
332         -- create one slice with testnode1, one with testnode2
333         worldedit.set(pos1, vecw(axis, b + 1, pos2), testnode1)
334         worldedit.set(vecw(axis, b + 2, pos1), pos2, testnode2)
335         worldedit.copy(pos1, pos2, axis, n)
336
337         -- should have three slices now
338         check.filled(pos1, vecw(axis, b + 1, pos2), testnode1)
339         check.filled(vecw(axis, b + 2, pos1), pos2, testnode1)
340         check.filled(vecw(axis, b + 4, pos1), vector.add(pos2, vecw(axis, n)), testnode2)
341         check.filled2(m, "air")
342 end)
343
344 register_test("worldedit.copy2", function()
345         local pos1, pos2 = area.get(6)
346         local m1 = area.margin(1)
347         local pos1_, pos2_ = area.get(6)
348         local m2 = area.margin(1)
349
350         local pattern = {testnode1, testnode2, testnode3, testnode1, testnode2}
351         place_pattern(pos1, pos2, pattern)
352         worldedit.copy2(pos1, pos2, vector.subtract(pos1_, pos1))
353
354         check.pattern(pos1, pos2, pattern)
355         check.pattern(pos1_, pos2_, pattern)
356         check.filled2(m1, "air")
357         check.filled2(m2, "air")
358 end)
359
360 register_test("worldedit.move (overlap)", function()
361         local pos1, pos2 = area.get(7)
362         local axis, n = area.dir(2)
363         local m = area.margin(1)
364
365         local pattern = {testnode2, testnode1, testnode2, testnode3, testnode3}
366         place_pattern(pos1, pos2, pattern)
367         worldedit.move(pos1, pos2, axis, n)
368
369         check.filled(pos1, vecw(axis, pos1[axis] + n - 1, pos2), "air")
370         check.pattern(vecw(axis, pos1[axis] + n, pos1), vecw(axis, pos2[axis] + n, pos2), pattern)
371         check.filled2(m, "air")
372 end)
373
374 register_test("worldedit.move", function()
375         local pos1, pos2 = area.get(10)
376         local axis, n = area.dir(10)
377         local m = area.margin(1)
378
379         local pattern = {testnode1, testnode3, testnode3, testnode2}
380         place_pattern(pos1, pos2, pattern)
381         worldedit.move(pos1, pos2, axis, n)
382
383         check.filled(pos1, pos2, "air")
384         check.pattern(vecw(axis, pos1[axis] + n, pos1), vecw(axis, pos2[axis] + n, pos2), pattern)
385         check.filled2(m, "air")
386 end)
387
388 -- TODO: the rest (also testing param2 + metadata)
389
390
391 ---------------------
392 -- Main function
393 ---------------------
394 worldedit.run_tests = function()
395         do
396                 local v = minetest.get_version()
397                 print("Running " .. #tests .. " tests for WorldEdit " ..
398                         worldedit.version_string .. " on " .. v.project .. " " .. (v.hash or v.string))
399         end
400
401         init_nodes()
402
403         -- emerge area from (0,0,0) ~ (56,56,56) and keep it loaded
404         -- Note: making this area smaller speeds up tests
405         local wanted = vec(56, 56, 56)
406         for x = 0, math.floor(wanted.x/16) do
407         for y = 0, math.floor(wanted.y/16) do
408         for z = 0, math.floor(wanted.z/16) do
409                 assert(minetest.forceload_block({x=x*16, y=y*16, z=z*16}, true))
410         end
411         end
412         end
413         area.assign(vec(0, 0, 0), wanted, function()
414
415                 local failed = 0
416                 for _, test in ipairs(tests) do
417                         if not test.func then
418                                 local s = "---- " .. test.name .. " "
419                                 print(s .. string.rep("-", 60 - #s))
420                         else
421                                 if not test.dry then
422                                         area.clear()
423                                 end
424                                 local ok, err = pcall(test.func)
425                                 print(string.format("%-60s %s", test.name, ok and "pass" or "FAIL"))
426                                 if not ok then
427                                         print("   " .. err)
428                                         failed = failed + 1
429                                 end
430                         end
431                 end
432
433                 print("Done, " .. failed .. " tests failed.")
434                 if failed == 0 then
435                         io.close(io.open(minetest.get_worldpath() .. "/tests_ok", "w"))
436                 end
437                 minetest.request_shutdown()
438         end)
439 end
440
441 -- for debug purposes
442 minetest.register_on_joinplayer(function(player)
443         minetest.set_player_privs(player:get_player_name(),
444                 minetest.string_to_privs("fly,fast,noclip,basic_debug,debug,interact"))
445 end)
446 minetest.register_on_punchnode(function(pos, node, puncher)
447         minetest.chat_send_player(puncher:get_player_name(), pos2str(pos))
448 end)