1local inprocess = require "specl.inprocess"
2local hell      = require "specl.shell"
3local std       = require "specl.std"
4
5badargs = require "specl.badargs"
6
7local top_srcdir = os.getenv "top_srcdir" or "."
8local top_builddir = os.getenv "top_builddir" or "."
9
10package.path = std.package.normalize (
11                 top_builddir .. "/lib/?.lua",
12                 top_builddir .. "/lib/?/init.lua",
13                 top_srcdir .. "/lib/?.lua",
14                 top_srcdir .. "/lib/?/init.lua",
15                 package.path
16               )
17
18
19-- Allow user override of LUA binary used by hell.spawn, falling
20-- back to environment PATH search for "lua" if nothing else works.
21local LUA = os.getenv "LUA" or "lua"
22
23
24-- Tweak _DEBUG without tripping over Specl nested environments.
25setdebug = require "std.debug"._setdebug
26
27
28-- Make sure we have a maxn even when _VERSION ~= 5.1
29-- @fixme remove this when we get unpack from specl.std
30maxn = table.maxn or function (t)
31  local n = 0
32  for k in pairs (t) do
33    if type (k) == "number" and k > n then n = k end
34  end
35  return n
36end
37
38
39-- Take care to always unpack upto the highest numeric index, for
40-- consistency across Lua versions.
41local _unpack = table.unpack or unpack
42
43-- @fixme pick this up from specl.std with the next release
44function unpack (t, i, j)
45  return _unpack (t, i or 1, j or maxn (t))
46end
47
48
49-- In case we're not using a bleeding edge release of Specl...
50badargs.result = badargs.result or function (fname, i, want, got)
51  if want == nil then i, want =  i - 1, i end -- numbers only for narg error
52
53  if got == nil and type (want) == "number" then
54    local s = "bad result #%d from '%s' (no more than %d result%s expected, got %d)"
55    return s:format (i + 1, fname, i, i == 1 and "" or "s", want)
56  end
57
58  local function showarg (s)
59    return ("|" .. s .. "|"):
60             gsub ("|%?", "|nil|"):
61	     gsub ("|nil|", "|no value|"):
62             gsub ("|any|", "|any value|"):
63             gsub ("|#", "|non-empty "):
64	     gsub ("|func|", "|function|"):
65	     gsub ("|file|", "|FILE*|"):
66	     gsub ("^|", ""):
67	     gsub ("|$", ""):
68	     gsub ("|([^|]+)$", "or %1"):
69	     gsub ("|", ", ")
70  end
71
72  return string.format ("bad result #%d from '%s' (%s expected, got %s)",
73                        i, fname, showarg (want), got or "no value")
74end
75
76
77-- Wrap up badargs function in a succinct single call.
78function init (M, mname, fname)
79  local name = (mname .. "." .. fname):gsub ("^%.", "")
80  return M[fname],
81         function (...) return badargs.format (name, ...) end,
82         function (...) return badargs.result (name, ...) end
83end
84
85
86-- A copy of base.lua:prototype, so that an unloadable base.lua doesn't
87-- prevent everything else from working.
88function prototype (o)
89  return (getmetatable (o) or {})._type or io.type (o) or type (o)
90end
91
92
93function nop () end
94
95
96-- Error message specifications use this to shorten argument lists.
97-- Copied from functional.lua to avoid breaking all tests if functional
98-- cannot be loaded correctly.
99function bind (f, fix)
100  return function (...)
101           local arg = {}
102           for i, v in pairs (fix) do
103             arg[i] = v
104           end
105           local i = 1
106           for _, v in pairs {...} do
107             while arg[i] ~= nil do i = i + 1 end
108             arg[i] = v
109           end
110           return f (unpack (arg))
111         end
112end
113
114
115local function mkscript (code)
116  local f = os.tmpname ()
117  local h = io.open (f, "w")
118  h:write (code)
119  h:close ()
120  return f
121end
122
123
124--- Run some Lua code with the given arguments and input.
125-- @string code valid Lua code
126-- @tparam[opt={}] string|table arg single argument, or table of
127--   arguments for the script invocation.
128-- @string[opt] stdin standard input contents for the script process
129-- @treturn specl.shell.Process|nil status of resulting process if
130--   execution was successful, otherwise nil
131function luaproc (code, arg, stdin)
132  local f = mkscript (code)
133  if type (arg) ~= "table" then arg = {arg} end
134  local cmd = {LUA, f, unpack (arg)}
135  -- inject env and stdin keys separately to avoid truncating `...` in
136  -- cmd constructor
137  cmd.env = { LUA_PATH=package.path, LUA_INIT="", LUA_INIT_5_2="" }
138  cmd.stdin = stdin
139  local proc = hell.spawn (cmd)
140  os.remove (f)
141  return proc
142end
143
144
145--- Concatenate the contents of listed existing files.
146-- @string ... names of existing files
147-- @treturn string concatenated contents of those files
148function concat_file_content (...)
149  local t = {}
150  for _, name in ipairs {...} do
151    h = io.open (name)
152    t[#t + 1] = h:read "*a"
153  end
154  return table.concat (t)
155end
156
157
158local function tabulate_output (code)
159  local proc = luaproc (code)
160  if proc.status ~= 0 then return error (proc.errout) end
161  local r = {}
162  proc.output:gsub ("(%S*)[%s]*",
163    function (x)
164      if x ~= "" then r[x] = true end
165    end)
166  return r
167end
168
169
170--- Show changes to tables wrought by a require statement.
171-- There are a few modes to this function, controlled by what named
172-- arguments are given.  Lists new keys in T1 after `require "import"`:
173--
174--     show_apis {added_to=T1, by=import}
175--
176-- List keys returned from `require "import"`, which have the same
177-- value in T1:
178--
179--     show_apis {from=T1, used_by=import}
180--
181-- List keys from `require "import"`, which are also in T1 but with
182-- a different value:
183--
184--     show_apis {from=T1, enhanced_by=import}
185--
186-- List keys from T2, which are also in T1 but with a different value:
187--
188--     show_apis {from=T1, enhanced_in=T2}
189--
190-- @tparam table argt one of the combinations above
191-- @treturn table a list of keys according to criteria above
192function show_apis (argt)
193  local added_to, from, not_in, enhanced_in, enhanced_after, by =
194    argt.added_to, argt.from, argt.not_in, argt.enhanced_in,
195    argt.enhanced_after, argt.by
196
197  if added_to and by then
198    return tabulate_output ([[
199      local before, after = {}, {}
200      for k in pairs (]] .. added_to .. [[) do
201        before[k] = true
202      end
203
204      local M = require "]] .. by .. [["
205      for k in pairs (]] .. added_to .. [[) do
206        after[k] = true
207      end
208
209      for k in pairs (after) do
210        if not before[k] then print (k) end
211      end
212    ]])
213
214  elseif from and not_in then
215    return tabulate_output ([[
216      local from = ]] .. from .. [[
217      local M = require "]] .. not_in .. [["
218
219      for k in pairs (M) do
220	-- M[1] is typically the module namespace name, don't match
221	-- that!
222        if k ~= 1 and from[k] ~= M[k] then print (k) end
223      end
224    ]])
225
226  elseif from and enhanced_in then
227    return tabulate_output ([[
228      local from = ]] .. from .. [[
229      local M = require "]] .. enhanced_in .. [["
230
231      for k, v in pairs (M) do
232        if from[k] ~= M[k] and from[k] ~= nil then print (k) end
233      end
234    ]])
235
236  elseif from and enhanced_after then
237    return tabulate_output ([[
238      local before, after = {}, {}
239      local from = ]] .. from .. [[
240
241      for k, v in pairs (from) do before[k] = v end
242      ]] .. enhanced_after .. [[
243      for k, v in pairs (from) do after[k] = v end
244
245      for k, v in pairs (before) do
246        if after[k] ~= nil and after[k] ~= v then print (k) end
247      end
248    ]])
249  end
250
251  assert (false, "missing argument to show_apis")
252end
253
254
255-- Stub inprocess.capture if necessary; new in Specl 12.
256capture = inprocess.capture or
257          function (f, arg) return nil, nil, f (unpack (arg or {})) end
258
259
260do
261  -- Custom matcher for set size and set membership.
262
263  local util     = require "specl.util"
264  local matchers = require "specl.matchers"
265
266  local Matcher, matchers, q =
267        matchers.Matcher, matchers.matchers, matchers.stringify
268
269  matchers.have_size = Matcher {
270    function (self, actual, expect)
271      local size = 0
272      for _ in pairs (actual) do size = size + 1 end
273      return size == expect
274    end,
275
276    actual = "table",
277
278    format_expect = function (self, expect)
279      return " a table containing " .. expect .. " elements, "
280    end,
281
282    format_any_of = function (self, alternatives)
283      return " a table with any of " ..
284             util.concat (alternatives, util.QUOTED) .. " elements, "
285    end,
286  }
287
288  matchers.have_member = Matcher {
289    function (self, actual, expect)
290      return actual[expect] ~= nil
291    end,
292
293    actual = "set",
294
295    format_expect = function (self, expect)
296      return " a set containing " .. q (expect) .. ", "
297    end,
298
299    format_any_of = function (self, alternatives)
300      return " a set containing any of " ..
301             util.concat (alternatives, util.QUOTED) .. ", "
302    end,
303  }
304
305  -- Alias that doesn't tickle sc_error_message_uppercase.
306  matchers.raise = matchers.error
307end
308