aboutsummaryrefslogtreecommitdiff
path: root/spec/util/quick.lua
diff options
context:
space:
mode:
Diffstat (limited to 'spec/util/quick.lua')
-rw-r--r--spec/util/quick.lua372
1 files changed, 372 insertions, 0 deletions
diff --git a/spec/util/quick.lua b/spec/util/quick.lua
new file mode 100644
index 00000000..9c88b719
--- /dev/null
+++ b/spec/util/quick.lua
@@ -0,0 +1,372 @@
1local quick = {}
2
3local dir_sep = package.config:sub(1, 1)
4
5local cfg = require("luarocks.core.cfg")
6local dir = require("luarocks.dir")
7local fs = require("luarocks.fs")
8local versions = require("spec.util.versions")
9cfg.init()
10fs.init()
11
12local function native_slash(pathname)
13 return (pathname:gsub("[/\\]", dir_sep))
14end
15
16local function parse_cmd(line)
17 local cmd, arg = line:match("^%s*([A-Z_]+):%s*(.*)%s*$")
18 return cmd, arg
19end
20
21local function is_blank(line)
22 return not not line:match("^%s*$")
23end
24
25local function is_hr(line)
26 return not not line:match("^%-%-%-%-%-")
27end
28
29local function parse(filename)
30 local fd = assert(io.open(filename, "r"))
31 local input = assert(fd:read("*a"))
32 fd:close()
33
34 local tests = {}
35
36 local cur_line = 0
37 local cur_test
38 local cur_op
39 local cur_block
40 local cur_block_name
41 local stack = { "start" }
42
43 local function start_test(arg)
44 cur_test = {
45 name = arg,
46 ops = {},
47 }
48 cur_op = nil
49 table.insert(tests, cur_test)
50 table.insert(stack, "test")
51 end
52
53 local function fail(msg)
54 io.stderr:write("Error reading " .. filename .. ":" .. cur_line .. ": " .. msg .. "\n")
55 os.exit(1)
56 end
57
58 local function bool_arg(cmd, cur_block, field, arg)
59 if arg ~= "true" and arg ~= "false" then
60 fail(cmd .. " argument must be 'true' or 'false'")
61 end
62 cur_block[field] = (arg == "true")
63 end
64
65 local test_env = require("spec.util.test_env")
66 local function expand_vars(line)
67 if not line then
68 return nil
69 end
70 return (line:gsub("%%%b{}", function(var)
71 var = var:sub(3, -2)
72 local fn, fnarg = var:match("^%s*([a-z_]+)%s*%(%s*([^)]+)%s*%)%s*$")
73
74 local value
75 if var == "tmpdir" then
76 value = "%{tmpdir}"
77 elseif var == "url(tmpdir)" then
78 value = "%{url(tmpdir)}"
79 elseif fn == "url" then
80 value = expand_vars(fnarg)
81 value = value:gsub("\\", "/")
82 elseif fn == "path" then
83 value = expand_vars(fnarg)
84 value = value:gsub("[/\\]", dir_sep)
85 elseif fn == "version" then
86 value = versions[fnarg:lower()] or ""
87 elseif fn == "version_" then
88 value = (versions[fnarg:lower()] or ""):gsub("[%.%-]", "_")
89 else
90 value = test_env.testing_paths[var]
91 or test_env.env_variables[var]
92 or test_env[var]
93 or ""
94 end
95
96 return value
97 end))
98 end
99
100 for line in input:gmatch("[^\n]*") do
101 cur_line = cur_line + 1
102
103 local state = stack[#stack]
104 if state == "start" then
105 local cmd, arg = parse_cmd(line)
106 if cmd == "TEST" then
107 start_test(arg)
108 elseif cmd then
109 fail("expected TEST, got " .. cmd)
110 elseif is_blank(line) then
111 -- skip blank lines and arbitrary text,
112 -- which is interpreted as a comment
113 end
114 elseif state == "test" then
115 local cmd, arg = parse_cmd(line)
116 arg = expand_vars(arg)
117 if cmd == "FILE" then
118 cur_op = {
119 op = "FILE",
120 name = arg,
121 data = {},
122 }
123 table.insert(cur_test.ops, cur_op)
124 cur_block = cur_op
125 cur_block_name = "FILE"
126 table.insert(stack, "block start")
127 elseif cmd == "RUN" then
128 local program, args = arg:match("([^ ]+)%s*(.*)$")
129 if not program then
130 fail("expected a program argument in RUN")
131 end
132
133 cur_op = {
134 op = "RUN",
135 exit = 0,
136 exit_line = cur_line,
137 line = cur_line,
138 program = program,
139 args = args,
140 }
141 table.insert(cur_test.ops, cur_op)
142 elseif cmd == "EXISTS" then
143 cur_op = {
144 op = "EXISTS",
145 file = dir.normalize(arg),
146 }
147 table.insert(cur_test.ops, cur_op)
148 elseif cmd == "NOT_EXISTS" then
149 cur_op = {
150 op = "NOT_EXISTS",
151 file = dir.normalize(arg),
152 }
153 table.insert(cur_test.ops, cur_op)
154 elseif cmd == "MKDIR" then
155 cur_op = {
156 op = "MKDIR",
157 file = dir.normalize(arg),
158 line = cur_line,
159 }
160 table.insert(cur_test.ops, cur_op)
161 elseif cmd == "EXIT" then
162 if not cur_op or cur_op.op ~= "RUN" then
163 fail("EXIT must be given in the context of a RUN")
164 end
165
166 local code = tonumber(arg)
167 if not code and not (code >= 0 and code <= 128) then
168 fail("EXIT code must be a number in the range 0-128, got " .. arg)
169 end
170
171 cur_op.exit = code
172 cur_op.exit_line = cur_line
173 elseif cmd == "STDERR" then
174 if not cur_op or cur_op.op ~= "RUN" then
175 fail("STDERR must be given in the context of a RUN")
176 end
177 if cur_op.stderr then
178 fail("STDERR was already declared")
179 end
180
181 cur_op.stderr = {
182 data = {}
183 }
184 cur_block = cur_op.stderr
185 cur_block_name = "STDERR"
186 table.insert(stack, "block start")
187 elseif cmd == "STDOUT" then
188 if not cur_op or cur_op.op ~= "RUN" then
189 fail("STDOUT must be given in the context of a RUN")
190 end
191 if cur_op.stdout then
192 fail("STDOUT was already declared")
193 end
194
195 cur_op.stdout = {
196 data = {}
197 }
198 cur_block = cur_op.stdout
199 cur_block_name = "STDOUT"
200 table.insert(stack, "block start")
201 elseif cmd == "TEST" then
202 table.remove(stack)
203 start_test(arg)
204 elseif cmd then
205 fail("expected a command, got " .. cmd)
206 else
207 -- skip blank lines and arbitrary text,
208 -- which is interpreted as a comment
209 end
210 elseif state == "block start" then
211 local cmd, arg = parse_cmd(line)
212 if is_blank(line) then
213 -- skip
214 elseif is_hr(line) then
215 stack[#stack] = "block data"
216 cur_block.start = cur_line
217 elseif cmd == "PLAIN" then
218 bool_arg("PLAIN", cur_block, "plain", arg)
219 else
220 fail("expected '-----' to start " .. cur_block_name .. " block")
221 end
222 elseif state == "block data" then
223 if is_hr(line) then
224 cur_block = nil
225 table.remove(stack)
226 else
227 if not cur_block.plain then
228 line = expand_vars(line)
229 end
230 table.insert(cur_block.data, line)
231 end
232 end
233 end
234
235 return tests
236end
237
238function quick.compile(filename, env)
239 local tests = parse(filename)
240
241-- local dev_null = (package.config:sub(1, 1) == "/")
242-- and "/dev/null"
243-- or "NUL"
244
245 local cmd_helpers = {
246 ["luarocks"] = "luarocks_cmd",
247 ["luarocks-admin"] = "luarocks_admin_cmd",
248 }
249
250 for tn, t in ipairs(tests) do
251 local code = {}
252 local function write(...)
253 table.insert(code, table.concat({...}))
254 end
255
256 write([=[ local test_env = require("spec.util.test_env") ]=])
257 write([=[ local lfs = require("lfs") ]=])
258 write([=[ local fs = require("lfs") ]=])
259 write([=[ local dir_sep = package.config:sub(1, 1) ]=])
260 write([=[ local luarocks_cmd = test_env.execute_helper(test_env.Q(test_env.testing_paths.lua) .. " " .. test_env.testing_paths.src_dir .. "/bin/luarocks", false, test_env.env_variables):sub(1, -5) ]=])
261 write([=[ local luarocks_admin_cmd = test_env.execute_helper(test_env.Q(test_env.testing_paths.lua) .. " " .. test_env.testing_paths.src_dir .. "/bin/luarocks-admin", false, test_env.env_variables):sub(1, -5) ]=])
262
263 write(([=[ local function error_message(line, msg, input) ]=]))
264 write(([=[ local out = {"\n\n", %q, ":", line, ": ", msg} ]=]):format(filename))
265 write(([=[ if input then ]=]))
266 write(([=[ if input:match("\n") then ]=]))
267 write(([=[ table.insert(out, "\n") ]=]))
268 write(([=[ table.insert(out, ("-"):rep(40)) ]=]))
269 write(([=[ table.insert(out, "\n") ]=]))
270 write(([=[ table.insert(out, input) ]=]))
271 write(([=[ table.insert(out, ("-"):rep(40)) ]=]))
272 write(([=[ table.insert(out, "\n") ]=]))
273 write(([=[ else ]=]))
274 write(([=[ table.insert(out, ": ") ]=]))
275 write(([=[ table.insert(out, input) ]=]))
276 write(([=[ end ]=]))
277 write(([=[ end ]=]))
278 write(([=[ return table.concat(out) ]=]))
279 write(([=[ end ]=]))
280
281 write([=[ return function() ]=])
282 write([=[ test_env.run_in_tmp(function(tmpdir) ]=])
283 write([=[ local function handle_tmpdir(s) ]=])
284 write([=[ return (s:gsub("%%{url%(tmpdir%)}", (tmpdir:gsub("\\", "/"))) ]=])
285 write([=[ :gsub("%%{tmpdir}", (tmpdir:gsub("[\\/]", dir_sep)))) ]=])
286 write([=[ end ]=])
287 for _, op in ipairs(t.ops) do
288 if op.op == "FILE" then
289 write([=[ test_env.write_file(handle_tmpdir("]=], op.name, [=["), handle_tmpdir([=====[ ]=])
290 for _, line in ipairs(op.data) do
291 write(line)
292 end
293 write([=[ ]=====]), finally) ]=])
294 elseif op.op == "EXISTS" then
295 write(([=[ assert.truthy(lfs.attributes(%q)) ]=]):format(op.file))
296 elseif op.op == "NOT_EXISTS" then
297 write(([=[ assert.falsy(lfs.attributes(%q)) ]=]):format(op.file))
298 elseif op.op == "MKDIR" then
299 local bits = {}
300 op.file = native_slash(op.file)
301 if op.file:sub(1, 1) == dir_sep then bits[1] = "" end
302 write([=[ local ok, err ]=])
303 for p in op.file:gmatch("[^" .. dir_sep .. "]+") do
304 table.insert(bits, p)
305 local d = table.concat(bits, dir_sep)
306 write(([=[ ok, err = lfs.mkdir(%q) ]=]):format(d, d))
307 end
308 write(([=[ assert.truthy((lfs.attributes(%q) or {}).mode == "directory", error_message(%d, "MKDIR failed: " .. %q .. " - " .. (err or "") )) ]=]):format(op.file, op.line, op.file))
309 elseif op.op == "RUN" then
310 local cmd_helper = cmd_helpers[op.program] or op.program
311 local redirs = " 1>stdout.txt 2>stderr.txt "
312 write(([=[ local ok, _, code = os.execute(%s .. " " .. %q .. %q) ]=]):format(cmd_helper, op.args, redirs))
313 write([=[ if type(ok) == "number" then code = (ok >= 256 and ok / 256 or ok) end ]=])
314
315 write([=[ local fd_stderr = assert(io.open("stderr.txt", "r")) ]=])
316 write([=[ local stderr_data = fd_stderr:read("*a") ]=])
317 write([=[ fd_stderr:close() ]=])
318
319 write([=[ if stderr_data:match("please report") then ]=])
320 write(([=[ assert(false, error_message(%d, "RUN crashed: ", stderr_data)) ]=]):format(op.line))
321 write([=[ end ]=])
322
323 if op.stdout then
324 write([=[ local fd_stdout = assert(io.open("stdout.txt", "r")) ]=])
325 write([=[ local stdout_data = fd_stdout:read("*a") ]=])
326 write([=[ fd_stdout:close() ]=])
327
328 write([=[ do ]=])
329 write([=[ local block_at = 1 ]=])
330 write([=[ local s, e, line ]=])
331 for i, line in ipairs(op.stdout.data) do
332 write(([=[ line = %q ]=]):format(line))
333 write(([=[ s, e = string.find(stdout_data, line, block_at, true) ]=]))
334 write(([=[ assert(s, error_message(%d, "STDOUT did not match: " .. line, stdout_data)) ]=]):format(op.stdout.start + i))
335 write(([=[ block_at = e + 1 ]=]):format(i))
336 end
337 write([=[ end ]=])
338 end
339
340 if op.stderr then
341 write([=[ do ]=])
342 write([=[ local block_at = 1 ]=])
343 write([=[ local s, e, line ]=])
344 for i, line in ipairs(op.stderr.data) do
345 write(([=[ line = %q ]=]):format(line))
346 write(([=[ s, e = string.find(stderr_data, line, block_at, true) ]=]))
347 write(([=[ assert(s, error_message(%d, "STDERR did not match: " .. line, stderr_data)) ]=]):format(op.stderr.start + i))
348 write(([=[ block_at = e + 1 ]=]):format(i))
349 end
350 write([=[ end ]=])
351 end
352
353 if op.exit then
354 write(([=[ assert.same(%d, code, error_message(%d, "EXIT did not match: " .. %d, stderr_data)) ]=]):format(op.exit, op.exit_line, op.exit))
355 end
356 end
357 end
358 write([=[ end) ]=])
359 write([=[ end ]=])
360
361 local program = table.concat(code, "\n")
362 local chunk = assert(load(program, "@" .. filename .. ": test " .. tn, "t", env or _ENV))
363 if env and setfenv then
364 setfenv(chunk, env)
365 end
366 t.fn = chunk()
367 end
368
369 return tests
370end
371
372return quick