xref: /freebsd/contrib/lyaml/spec/spec_helper.lua (revision 2bc180ef045e5911cce0cea1c2a139cffd2b577a)
1--[[
2 LYAML binding for Lua 5.1, 5.2, 5.3 & 5.4
3 Copyright (C) 2013-2022 Gary V. Vaughan
4]]
5
6do
7   local std = require 'specl.std'
8   local spawn = require 'specl.shell'.spawn
9   local objdir = spawn('./build-aux/luke --value=objdir').output
10
11
12   package.path = std.package.normalize(
13      './lib/?.lua',
14      './lib/?/init.lua',
15      package.path
16   )
17   package.cpath = std.package.normalize(
18      './' .. objdir:match("^objdir='(.*)'") .. '/?.so',
19      './' .. objdir:match("^objdir='(.*)'") .. '/?.dll',
20      package.cpath
21   )
22end
23
24local hell = require 'specl.shell'
25
26
27yaml = require 'yaml'
28
29BOM = string.char(254, 255) -- UTF-16 Byte Order Mark
30
31-- Allow use of bare 'pack' and 'unpack' even in Lua > 5.2.
32pack = table.pack or function(...) return {n = select('#', ...), ...} end
33unpack = table.unpack or unpack
34list = pack
35
36
37function dump(e)
38   print(std.string.prettytostring(e))
39end
40
41
42function github_issue(n)
43  return 'see http://github.com/gvvaughan/lyaml/issues/' .. tostring(n)
44end
45
46
47-- Output a list of event tables to the given emitter.
48function emitevents(emitter, list)
49   for _, v in ipairs(list) do
50      if type(v) == 'string' then
51         ok, msg = emitter.emit {type=v}
52      elseif type(v) == 'table' then
53         ok, msg = emitter.emit(v)
54      else
55         error 'expected table or string argument'
56      end
57
58      if not ok then
59         error(msg)
60      elseif ok and msg then
61         return msg
62      end
63   end
64end
65
66
67-- Create a new emitter and send STREAM_START, listed events and STREAM_END.
68function emit(list)
69   local emitter = yaml.emitter()
70   emitter.emit {type='STREAM_START'}
71   emitevents(emitter, list)
72   local _, msg = emitter.emit {type='STREAM_END'}
73   return msg
74end
75
76
77-- Create a new parser for STR, and consume the first N events.
78function consume(n, str)
79   local e = yaml.parser(str)
80   for n = 1, n do
81      e()
82   end
83   return e
84end
85
86
87-- Return a new table with only elements of T that have keys listed
88-- in the following arguments.
89function filter(t, ...)
90   local u = {}
91   for _, k in ipairs {...} do
92      u[k] = t[k]
93   end
94   return u
95end
96
97
98function iscallable(x)
99   return type(x) == 'function' or type((getmetatable(x) or {}).__call) == 'function'
100end
101
102
103local function mkscript(code)
104   local f = os.tmpname()
105   local h = io.open(f, 'w')
106   -- TODO: Move this into specl, or expose arguments so that we can
107   --          turn this on and off based on specl `--coverage` arg.
108   h:write "pcall(require, 'luacov')"
109   h:write(code)
110   h:close()
111   return f
112end
113
114
115-- Allow user override of LUA binary used by hell.spawn, falling
116-- back to environment PATH search for 'lua' if nothing else works.
117local LUA = os.getenv 'LUA' or 'lua'
118
119
120--- Run some Lua code with the given arguments and input.
121-- @string code valid Lua code
122-- @tparam[opt={}] string|table arg single argument, or table of
123--    arguments for the script invocation.
124-- @string[opt] stdin standard input contents for the script process
125-- @treturn specl.shell.Process|nil status of resulting process if
126--    execution was successful, otherwise nil
127function luaproc(code, arg, stdin)
128   local f = mkscript(code)
129   if type(arg) ~= 'table' then arg = {arg} end
130   local cmd = {LUA, f, unpack(arg)}
131   -- inject env and stdin keys separately to avoid truncating `...` in
132   -- cmd constructor
133   cmd.env = { LUA_PATH=package.path, LUA_INIT='', LUA_INIT_5_2='' }
134   cmd.stdin = stdin
135   local proc = hell.spawn(cmd)
136   os.remove(f)
137   return proc
138end
139
140
141local function tabulate_output(code)
142   local proc = luaproc(code)
143   if proc.status ~= 0 then return error(proc.errout) end
144   local r = {}
145   proc.output:gsub('(%S*)[%s]*',
146      function(x)
147         if x ~= '' then r[x] = true end
148      end)
149   return r
150end
151
152
153--- Show changes to tables wrought by a require statement.
154-- There are a few modes to this function, controlled by what named
155-- arguments are given.   Lists new keys in T1 after `require "import"`:
156--
157--       show_apis {added_to=T1, by=import}
158--
159-- @tparam table argt one of the combinations above
160-- @treturn table a list of keys according to criteria above
161function show_apis(argt)
162   return tabulate_output([[
163      local before, after = {}, {}
164      for k in pairs(]] .. argt.added_to .. [[) do
165         before[k] = true
166      end
167
168      local M = require ']] .. argt.by .. [['
169      for k in pairs(]] .. argt.added_to .. [[) do
170         after[k] = true
171      end
172
173      for k in pairs(after) do
174         if not before[k] then print(k) end
175      end
176   ]])
177end
178
179
180
181--[[ ========= ]]--
182--[[ Call Spy. ]]--
183--[[ ========= ]]--
184
185
186spy = function(fn)
187   return setmetatable({}, {
188      __call = function(self, ...)
189         self[#self + 1] = list(...)
190         return fn(...)
191      end,
192   })
193end
194
195
196do
197   --[[ ================ ]]--
198   --[[ Custom matchers. ]]--
199   --[[ ================ ]]--
200
201   local matchers = require 'specl.matchers'
202   local eqv = require 'specl.std'.operator.eqv
203   local str = require 'specl.std'.string.tostring
204
205   local Matcher, matchers = matchers.Matcher, matchers.matchers
206   local concat = table.concat
207
208
209   matchers.be_called_with = Matcher {
210      function(self, actual, expected)
211         for i,v in ipairs(expected or {}) do
212            if not eqv(actual[i], v) then
213               return false
214            end
215         end
216         return true
217      end,
218
219      actual = 'argmuents',
220
221      format_expect = function(self, expect)
222         return ' arguments (' .. str(expect) .. '), '
223      end,
224   }
225
226   matchers.be_callable = Matcher {
227      function(self, actual, _)
228         return iscallable(actual)
229      end,
230
231      actual = 'callable',
232
233      format_expect = function(self, expect)
234         return ' callable, '
235      end,
236   }
237
238   matchers.be_falsey = Matcher {
239      function(self, actual, _)
240         return not actual and true or false
241      end,
242
243      actual = 'falsey',
244
245      format_expect = function(self, expect)
246         return ' falsey, '
247      end,
248   }
249
250   matchers.be_truthy = Matcher {
251      function(self, actual, _)
252         return actual and true or false
253      end,
254
255      actual = 'truthy',
256
257      format_expect = function(self, expect)
258         return ' truthy, '
259      end,
260   }
261
262   matchers.have_type = Matcher {
263      function(self, actual, expected)
264         return type(actual) == expected or (getmetatable(actual) or {})._type == expected
265      end,
266
267      actual = 'type',
268
269      format_expect = function(self, expect)
270         local article = 'a'
271         if match(expect, '^[aehiou]') then
272            article = 'an'
273         end
274         return concat{' ', article, ' ', expect, ', '}
275      end
276   }
277end
278