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