diff options
Diffstat (limited to 'Data/BuiltIn/Libraries/lua-stdlib/spec')
-rw-r--r-- | Data/BuiltIn/Libraries/lua-stdlib/spec/debug_spec.yaml | 222 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-stdlib/spec/io_spec.yaml | 439 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-stdlib/spec/math_spec.yaml | 99 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-stdlib/spec/package_spec.yaml | 202 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-stdlib/spec/spec_helper.lua | 416 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-stdlib/spec/std_spec.yaml | 444 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-stdlib/spec/string_spec.yaml | 549 | ||||
-rw-r--r-- | Data/BuiltIn/Libraries/lua-stdlib/spec/table_spec.yaml | 589 |
8 files changed, 2960 insertions, 0 deletions
diff --git a/Data/BuiltIn/Libraries/lua-stdlib/spec/debug_spec.yaml b/Data/BuiltIn/Libraries/lua-stdlib/spec/debug_spec.yaml new file mode 100644 index 0000000..5102723 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-stdlib/spec/debug_spec.yaml @@ -0,0 +1,222 @@ +# General Lua Libraries for Lua 5.1, 5.2 & 5.3 +# Copyright (C) 2011-2018 stdlib authors + +before: | + base_module = 'debug' + this_module = 'std.debug' + global_table = '_G' + + extend_base = {'getfenv', 'setfenv', 'say', 'trace'} + + M = require(this_module) + + +specify std.debug: +- context when required: + - context by name: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core debug table: + expect(show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core debug table: + expect(show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of(extend_base) + + - context via the std module: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by='std'}). + to_equal {} + - it does not touch the core debug table: + expect(show_apis {added_to=base_module, by='std'}). + to_equal {} + + +- describe debug: + - it does nothing when std._debug is disabled: + expect(luaproc [[ + require 'std._debug'(false) + require 'std.debug'('nothing to see here') + ]]).not_to_contain_error 'nothing to see here' + - it writes to stderr when std._debug is not set: + expect(luaproc [[ + require 'std.debug'('debugging') + ]]).to_contain_error 'debugging' + - it writes to stderr when std._debug is enabled: + expect(luaproc [[ + require 'std._debug'(true) + require 'std.debug'('debugging') + ]]).to_contain_error 'debugging' + - it writes to stderr when std._debug.level is not set: + expect(luaproc [[ + require 'std._debug'() + require 'std.debug'('debugging') + ]]).to_contain_error 'debugging' + - it writes to stderr when std._debug.level is specified: + expect(luaproc [[ + require 'std._debug'.level = 0 + require 'std.debug'('debugging') + ]]).to_contain_error 'debugging' + expect(luaproc [[ + require 'std._debug'.level = 1 + require 'std.debug'('debugging') + ]]).to_contain_error 'debugging' + expect(luaproc [[ + require 'std._debug'.level = 2 + require 'std.debug'('debugging') + ]]).to_contain_error 'debugging' + + +- describe say: + - it uses normalize.str: + expect(luaproc [[require 'std.debug'.say {'debugging'}]]). + to_contain_error(require 'std.normalize'.str {'debugging'}) + - context when std._debug is disabled: + - before: + preamble = [[ + require 'std._debug'(false) + ]] + - it does nothing when message level is not set: + expect(luaproc(preamble .. [[ + require 'std.debug'.say 'nothing to see here' + ]])).not_to_contain_error 'nothing to see here' + - it does nothing when message is set: + for _, level in next, {-999, 0, 1, 2, 999} do + expect(luaproc(preamble .. [[ + require 'std.debug'.say(]] .. level .. [[, 'nothing to see here') + ]])).not_to_contain_error 'nothing to see here' + end + - context when std._debug is not set: + - it writes to stderr when message level is not set: + expect(luaproc [[ + require 'std.debug'.say 'debugging' + ]]).to_contain_error 'debugging' + - it writes to stderr when message level is 1 or lower: + for _, level in next, {-999, 0, 1} do + expect(luaproc([[ + require 'std.debug'.say(]] .. level .. [[, 'debugging') + ]])).to_contain_error 'debugging' + end + - it does nothing when message level is 2 or higher: + for _, level in next, {2, 999} do + expect(luaproc([[ + require 'std.debug'.say(]] .. level .. [[, 'nothing to see here') + ]])).not_to_contain_error 'nothing to see here' + end + - context when std._debug is enabled: + - before: + preamble = [[ + require 'std._debug'(true) + ]] + - it writes to stderr when message level is not set: + expect(luaproc(preamble .. [[ + require 'std.debug'.say 'debugging' + ]])).to_contain_error 'debugging' + - it writes to stderr when message level is 1 or lower: + for _, level in next, {-999, 0, 1} do + expect(luaproc(preamble .. [[ + require 'std.debug'.say(]] .. level .. [[, 'debugging') + ]])).to_contain_error 'debugging' + end + - it does nothing when message level is 2 or higher: + for _, level in next, {2, 999} do + expect(luaproc(preamble .. [[ + require 'std.debug'.say(]] .. level .. [[, 'nothing to see here') + ]])).not_to_contain_error 'nothing to see here' + end + - context when std._debug.level is not set: + - it writes to stderr when message level is not set: + expect(luaproc [[ + require 'std.debug'.say 'debugging' + ]]).to_contain_error 'debugging' + - it writes to stderr when message level is 1 or lower: + for _, level in next, {-999, 0, 1} do + expect(luaproc([[ + require 'std.debug'.say(]] .. level .. [[, 'debugging') + ]])).to_contain_error 'debugging' + end + - it does nothing when message level is 2 or higher: + for _, level in next, {2, 999} do + expect(luaproc([[ + require 'std.debug'.say(]] .. level .. [[, 'nothing to see here') + ]])).not_to_contain_error 'nothing to see here' + end + - context when std._debug.level is specified: + - it writes to stderr when message level is 1 or lower: + for _, level in next, {0, 1, 2} do + expect(luaproc([[ + require 'std._debug'.level = ]] .. level .. [[ + require 'std.debug'.say 'debugging' + ]])).to_contain_error 'debugging' + end + - it does nothing when message level is higher than debug level: + expect(luaproc [[ + require 'std._debug'.level = 2 + require 'std.debug'.say(3, 'nothing to see here') + ]]).not_to_contain_error 'nothing to see here' + - it writes to stderr when message level equals debug level: + expect(luaproc [[ + require 'std._debug'.level = 2 + require 'std.debug'.say(2, 'debugging') + ]]).to_contain_error 'debugging' + - it writes to stderr when message level is lower than debug level: + expect(luaproc [[ + require 'std._debug'.level = 2 + require 'std.debug'.say(1, 'debugging') + ]]).to_contain_error 'debugging' + + +- describe trace: + - before: + f = init(M, this_module, 'trace') + + - it does nothing when debug hint is disabled: + expect(luaproc [[ + require 'std._debug'(false) + require 'std.debug' + os.exit(0) + ]]).to_succeed_with '' + - it does nothing when debug hint is not set: + expect(luaproc [[ + require 'std.debug' + os.exit(0) + ]]).to_succeed_with '' + - it does nothing when debug hint is enabled: + expect(luaproc [[ + require 'std._debug'(true) + require 'std.debug' + os.exit(0) + ]]).to_succeed_with '' + - it enables automatically when std._debug.call is set: | + expect(luaproc [[ + require 'std._debug'.call = true + require 'std.debug' + os.exit(1) + ]]).to_fail_while_containing ':3 call exit' + - it is enabled manually with debug.sethook: | + expect(luaproc [[ + local debug = require 'std.debug' + debug.sethook(debug.trace, 'cr') + os.exit(1) + ]]).to_fail_while_containing ':3 call exit' + - it writes call trace log to standard error: | + expect(luaproc [[ + local debug = require 'std.debug' + debug.sethook(debug.trace, 'cr') + os.exit(0) + ]]).to_contain_error ':3 call exit' + - it traces lua calls: | + expect(luaproc [[ + local debug = require 'std.debug' -- line 1 + local function incr(i) return i + 1 end -- line 2 + debug.sethook(debug.trace, 'cr') -- line 3 + os.exit(incr(41)) -- line 4 + ]]).to_fail_while_matching '.*:4 call incr <2:.*:4 return incr <2:.*' + - it traces C api calls: | + expect(luaproc [[ + local debug = require 'std.debug' + local function incr(i) return i + 1 end + debug.sethook(debug.trace, 'cr') + os.exit(incr(41)) + ]]).to_fail_while_matching '.*:4 call exit %[C%]%s$' diff --git a/Data/BuiltIn/Libraries/lua-stdlib/spec/io_spec.yaml b/Data/BuiltIn/Libraries/lua-stdlib/spec/io_spec.yaml new file mode 100644 index 0000000..67b850f --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-stdlib/spec/io_spec.yaml @@ -0,0 +1,439 @@ +# General Lua Libraries for Lua 5.1, 5.2 & 5.3 +# Copyright (C) 2011-2018 stdlib authors + +before: | + base_module = 'io' + this_module = 'std.io' + global_table = '_G' + + extend_base = {'catdir', 'catfile', 'die', 'dirname', + 'process_files', 'readlines', 'shell', 'slurp', + 'splitdir', 'warn', 'writelines'} + + dirsep = string.match(package.config, '^([^\n]+)\n') + + M = require(this_module) + + +specify std.io: +- context when required: + - context by name: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core io table: + expect(show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core io table: + expect(show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of(extend_base) + + - context via the std module: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by='std'}). + to_equal {} + - it does not touch the core io table: + expect(show_apis {added_to=base_module, by='std'}). + to_equal {} + + +- describe catdir: + - before: | + f = M.catdir + + - context with bad arguments: + badargs.diagnose(f, 'std.io.catdir(string*)') + + - it treats initial empty string as root directory: + expect(f('')).to_be(dirsep) + expect(f('', '')).to_be(dirsep) + expect(f('', 'root')).to_be(dirsep .. 'root') + - it returns a single argument unchanged: + expect(f('hello')).to_be 'hello' + - it joins multiple arguments with platform directory separator: + expect(f('one', 'two')).to_be('one' .. dirsep .. 'two') + expect(f('1', '2', '3', '4', '5')). + to_be(table.concat({'1', '2', '3', '4', '5'}, dirsep)) + + +- describe catfile: + - before: + f = M.catfile + + - context with bad arguments: + badargs.diagnose(f, 'std.io.catfile(string*)') + + - it treats initial empty string as root directory: + expect(f('', '')).to_be(dirsep) + expect(f('', 'root')).to_be(dirsep .. 'root') + - it returns a single argument unchanged: + expect(f('')).to_be '' + expect(f('hello')).to_be 'hello' + - it joins multiple arguments with platform directory separator: + expect(f('one', 'two')).to_be('one' .. dirsep .. 'two') + expect(f('1', '2', '3', '4', '5')). + to_be(table.concat({'1', '2', '3', '4', '5'}, dirsep)) + + +- describe die: + - before: | + script = [[require 'std.io'.die "By 'eck!"]] + + f = M.die + + - context with bad arguments: + badargs.diagnose(f, 'std.io.die(string, ?any*)') + + - it outputs a message to stderr: | + expect(luaproc(script)).to_fail_while_matching ": By 'eck!\n" + - it ignores `prog.line` without `prog.file` or `prog.name`: | + script = [[prog = {line=125};]] .. script + expect(luaproc(script)).to_fail_while_matching ": By 'eck!\n" + - it ignores `opts.line` without `opts.program`: | + script = [[opts = {line=99};]] .. script + expect(luaproc(script)).to_fail_while_matching ": By 'eck!\n" + - it prefixes `prog.name` if any: | + script = [[prog = {name='name'};]] .. script + expect(luaproc(script)).to_fail_while_matching ": name: By 'eck!\n" + - it appends `prog.line` if any, to `prog.name`: | + script = [[prog = {line=125, name='name'};]] .. script + expect(luaproc(script)).to_fail_while_matching ": name:125: By 'eck!\n" + - it prefixes `prog.file` if any: | + script = [[prog = {file='file'};]] .. script + expect(luaproc(script)).to_fail_while_matching ": file: By 'eck!\n" + - it appends `prog.line` if any, to `prog.name`: | + script = [[prog = {file='file', line=125};]] .. script + expect(luaproc(script)).to_fail_while_matching ": file:125: By 'eck!\n" + - it prefers `prog.name` to `prog.file` or `opts.program`: | + script = [[ + prog = {file='file', name='name'} + opts = {program='program'} + ]] .. script + expect(luaproc(script)).to_fail_while_matching ": name: By 'eck!\n" + - it appends `prog.line` if any to `prog.name` over anything else: | + script = [[ + prog = {file='file', line=125, name='name'} + opts = {line=99, program='program'} + ]] .. script + expect(luaproc(script)).to_fail_while_matching ": name:125: By 'eck!\n" + - it prefers `prog.file` to `opts.program`: | + script = [[ + prog = {file='file'}; opts = {program='program'} + ]] .. script + expect(luaproc(script)).to_fail_while_matching ": file: By 'eck!\n" + - it appends `prog.line` if any to `prog.file` over using `opts`: | + script = [[ + prog = {file='file', line=125} + opts = {line=99, program='program'} + ]] .. script + expect(luaproc(script)).to_fail_while_matching ": file:125: By 'eck!\n" + - it prefixes `opts.program` if any: | + script = [[opts = {program='program'};]] .. script + expect(luaproc(script)).to_fail_while_matching ": program: By 'eck!\n" + - it appends `opts.line` if any, to `opts.program`: | + script = [[opts = {line=99, program='program'};]] .. script + expect(luaproc(script)).to_fail_while_matching ": program:99: By 'eck!\n" + + +- describe dirname: + - before: + f = M.dirname + path = table.concat({'', 'one', 'two', 'three'}, dirsep) + + - context with bad arguments: + badargs.diagnose(f, 'std.io.dirname(string)') + + - it removes final separator and following: + expect(f(path)).to_be(table.concat({'', 'one', 'two'}, dirsep)) + + +- describe process_files: + - before: + name = 'Makefile' + names = {'LICENSE.md', 'Makefile', 'README.md'} + ascript = [[ + require 'std.io'.process_files(function(a) + print(a) + end) + ]] + lscript = [[ + require 'std.io'.process_files('=print(_1)') + ]] + iscript = [[ + require 'std.io'.process_files(function(_, i) + print(i) + end) + ]] + catscript = [[ + require 'std.io'.process_files(function() + io.write(io.input():read '*a') + end) + ]] + + f = M.process_files + + - context with bad arguments: | + badargs.diagnose(f, 'std.io.process_files(func)') + + examples { + ["it diagnoses non-file 'arg' elements"] = function() + expect(luaproc(ascript, 'not-an-existing-file')).to_contain_error.any_of { + "cannot open file 'not-an-existing-file'", -- Lua 5.2 + "bad argument #1 to 'input' (not-an-existing-file:", -- Lua 5.1 + } + end + } + + - it defaults to `-` if no arguments were passed: + expect(luaproc(ascript)).to_output '-\n' + - it iterates over arguments with supplied function: + expect(luaproc(ascript, name)).to_output(name .. '\n') + expect(luaproc(ascript, names)). + to_output(table.concat(names, '\n') .. '\n') + - it passes argument numbers to supplied function: + expect(luaproc(iscript, names)).to_output '1\n2\n3\n' + - it sets each file argument as the default input: + expect(luaproc(catscript, name)).to_output(concat_file_content(name)) + expect(luaproc(catscript, names)). + to_output(concat_file_content(unpack(names))) + - it processes io.stdin if no arguments were passed: + ## FIXME: where does that closing newline come from?? + expect(luaproc(catscript, nil, 'some\nlines\nof input')).to_output 'some\nlines\nof input\n' + - it processes io.stdin for `-` argument: + ## FIXME: where does that closing newline come from?? + expect(luaproc(catscript, '-', 'some\nlines\nof input')).to_output 'some\nlines\nof input\n' + + +- describe readlines: + - before: | + name = 'Makefile' + h = io.open(name) + lines = {} + for l in h:lines() do + lines[#lines + 1] = l + end + h:close() + + defaultin = io.input() + + f, badarg = init(M, this_module, 'readlines') + - after: + if io.type(defaultin) ~= 'closed file' then + io.input(defaultin) + end + + - context with bad arguments: | + badargs.diagnose(f, 'std.io.readlines(?file|string)') + + if have_typecheck then + examples { + ['it diagnoses non-existent file'] = function() + expect(f 'not-an-existing-file'). + to_raise "bad argument #1 to 'std.io.readlines'(" -- system dependent error message + end + } + closed = io.open(name, 'r') closed:close() + examples { + ['it diagnoses closed file argument'] = function() + expect(f(closed)).to_raise(badarg(1, '?file|string', 'closed file')) + end + } + end + + - it closes file handle upon completion: + h = io.open(name) + expect(io.type(h)).not_to_be 'closed file' + f(h) + expect(io.type(h)).to_be 'closed file' + - it reads lines from an existing named file: + expect(f(name)).to_equal(lines) + - it reads lines from an open file handle: + expect(f(io.open(name))).to_equal(lines) + - it reads from default input stream with no arguments: + io.input(name) + expect(f()).to_equal(lines) + + +- describe shell: + - before: + f = M.shell + + - context with bad arguments: + badargs.diagnose(f, 'std.io.shell(string)') + + - it returns the output from a shell command string: + expect(f [[printf '%s\n' 'foo' 'bar']]).to_be 'foo\nbar\n' + + +- describe slurp: + - before: | + name = 'Makefile' + h = io.open(name) + content = h:read '*a' + h:close() + + defaultin = io.input() + f, badarg = init(M, this_module, 'slurp') + - after: + if io.type(defaultin) ~= 'closed file' then + io.input(defaultin) + end + + - context with bad arguments: | + badargs.diagnose(f, 'std.io.slurp(?file|string)') + + if have_typecheck then + examples { + ['it diagnoses non-existent file'] = function() + expect(f 'not-an-existing-file'). + to_raise "bad argument #1 to 'std.io.slurp'(" -- system dependent error message + end + } + closed = io.open(name, 'r') closed:close() + examples { + ['it diagnoses closed file argument'] = function() + expect(f(closed)).to_raise(badarg(1, '?file|string', 'closed file')) + end + } + end + + - it reads content from an existing named file: + expect(f(name)).to_be(content) + - it reads content from an open file handle: + expect(f(io.open(name))).to_be(content) + - it closes file handle upon completion: + h = io.open(name) + expect(io.type(h)).not_to_be 'closed file' + f(h) + expect(io.type(h)).to_be 'closed file' + - it reads from default input stream with no arguments: + io.input(name) + expect(f()).to_be(content) + + +- describe splitdir: + - before: + f = M.splitdir + + - context with bad arguments: + badargs.diagnose(f, 'std.io.splitdir(string)') + + - it returns a filename as a one element list: + expect(f('hello')).to_equal {'hello'} + - it splits root directory in two empty elements: + expect(f(dirsep)).to_equal {'', ''} + - it returns initial empty string for absolute path: + expect(f(dirsep .. 'root')).to_equal {'', 'root'} + - it returns multiple components split at platform directory separator: + expect(f('one' .. dirsep .. 'two')).to_equal {'one', 'two'} + expect(f(table.concat({'1', '2', '3', '4', '5'}, dirsep))). + to_equal {'1', '2', '3', '4', '5'} + + +- describe warn: + - before: + script = [[require 'std.io'.warn 'Ayup!']] + f = M.warn + + - context with bad arguments: + badargs.diagnose(f, 'std.io.warn(string, ?any*)') + + - it outputs a message to stderr: + expect(luaproc(script)).to_output_error 'Ayup!\n' + - it ignores `prog.line` without `prog.file`, `prog.name` or `opts.program`: + script = [[prog = {line=125};]] .. script + expect(luaproc(script)).to_output_error 'Ayup!\n' + - it prefixes `prog.name` if any: | + script = [[prog = {name='name'};]] .. script + expect(luaproc(script)).to_output_error 'name: Ayup!\n' + - it appends `prog.line` if any, to `prog.name`: | + script = [[prog = {line=125, name='name'};]] .. script + expect(luaproc(script)).to_output_error 'name:125: Ayup!\n' + - it prefixes `prog.file` if any: | + script = [[prog = {file='file'};]] .. script + expect(luaproc(script)).to_output_error 'file: Ayup!\n' + - it appends `prog.line` if any, to `prog.name`: | + script = [[prog = {file='file', line=125};]] .. script + expect(luaproc(script)).to_output_error 'file:125: Ayup!\n' + - it prefers `prog.name` to `prog.file` or `opts.program`: | + script = [[ + prog = {file='file', name='name'} + opts = {program='program'} + ]] .. script + expect(luaproc(script)).to_output_error 'name: Ayup!\n' + - it appends `prog.line` if any to `prog.name` over anything else: | + script = [[ + prog = {file='file', line=125, name='name'} + opts = {line=99, program='program'} + ]] .. script + expect(luaproc(script)).to_output_error 'name:125: Ayup!\n' + - it prefers `prog.file` to `opts.program`: | + script = [[ + prog = {file='file'}; opts = {program='program'} + ]] .. script + expect(luaproc(script)).to_output_error 'file: Ayup!\n' + - it appends `prog.line` if any to `prog.file` over using `opts`: | + script = [[ + prog = {file='file', line=125} + opts = {line=99, program='program'} + ]] .. script + expect(luaproc(script)).to_output_error 'file:125: Ayup!\n' + - it prefixes `opts.program` if any: | + script = [[opts = {program='program'};]] .. script + expect(luaproc(script)).to_output_error 'program: Ayup!\n' + - it appends `opts.line` if any, to `opts.program`: | + script = [[opts = {line=99, program='program'};]] .. script + expect(luaproc(script)).to_output_error 'program:99: Ayup!\n' + + +- describe writelines: + - before: | + name = os.tmpname() + h = io.open(name, 'w') + lines = M.readlines(io.open 'Makefile') + + defaultout = io.output() + f, badarg = init(M, this_module, 'writelines') + - after: + if io.type(defaultout) ~= 'closed file' then + io.output(defaultout) + end + h:close() + os.remove(name) + + - context with bad arguments: + - 'it diagnoses argument #1 type not FILE*, string, number or nil': + if have_typecheck then + expect(f(false)).to_raise(badarg(1, '?file|string|number', 'boolean')) + end + - 'it diagnoses argument #2 type not string, number or nil': + if have_typecheck then + expect(f(1, false)).to_raise(badarg(2, 'string|number', 'boolean')) + end + - 'it diagnoses argument #3 type not string, number or nil': + if have_typecheck then + expect(f(1, 2, false)).to_raise(badarg(3, 'string|number', 'boolean')) + end + - it diagnoses closed file argument: | + closed = io.open(name, 'r') closed:close() + if have_typecheck then + expect(f(closed)).to_raise(badarg(1, '?file|string|number', 'closed file')) + end + + - it does not close the file handle upon completion: + expect(io.type(h)).not_to_be 'closed file' + f(h, 'foo') + expect(io.type(h)).not_to_be 'closed file' + - it writes lines to an open file handle: + f(h, unpack(lines)) + h:flush() + expect(M.readlines(io.open(name))).to_equal(lines) + - it accepts number valued arguments: + f(h, 1, 2, 3) + h:flush() + expect(M.readlines(io.open(name))).to_equal {'1', '2', '3'} + - it writes to default output stream with non-file first argument: + io.output(h) + f(unpack(lines)) + h:flush() + expect(M.readlines(io.open(name))).to_equal(lines) diff --git a/Data/BuiltIn/Libraries/lua-stdlib/spec/math_spec.yaml b/Data/BuiltIn/Libraries/lua-stdlib/spec/math_spec.yaml new file mode 100644 index 0000000..ed08753 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-stdlib/spec/math_spec.yaml @@ -0,0 +1,99 @@ +# General Lua Libraries for Lua 5.1, 5.2 & 5.3 +# Copyright (C) 2011-2018 stdlib authors + +before: + base_module = 'math' + this_module = 'std.math' + global_table = '_G' + + extend_base = {'floor', 'round'} + + M = require(this_module) + + +specify std.math: +- context when required: + - context by name: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core math table: + expect(show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core math table: + expect(show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of(extend_base) + + - context via the std module: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by='std'}). + to_equal {} + - it does not touch the core math table: + expect(show_apis {added_to=base_module, by='std'}). + to_equal {} + + +- describe floor: + - before: + f = M.floor + + - context with bad arguments: + badargs.diagnose(f, 'std.math.floor(number, ?int)') + + - it rounds to the nearest smaller integer: + expect(f(1.2)).to_be(1) + expect(f(1.9)).to_be(1) + expect(f(999e-2)).to_be(9) + expect(f(999e-3)).to_be(0) + - it rounds down to specified number of decimal places: + expect(f(1.2345, 0)).to_be(1.0) + expect(f(1.2345, 1)).to_be(1.2) + expect(f(1.2345, 2)).to_be(1.23) + expect(f(9.9999, 2)).to_be(9.99) + expect(f(99999e-3, 3)).to_be(99999e-3) + expect(f(99999e-4, 3)).to_be(9999e-3) + expect(f(99999e-5, 3)).to_be(999e-3) + + +- describe round: + - before: + f = M.round + + - context with bad arguments: + badargs.diagnose(f, 'std.math.round(number, ?int)') + + - it rounds to the nearest integer: + expect(f(1.2)).to_be(1) + expect(f(1.9)).to_be(2) + expect(f(949e-2)).to_be(9) + expect(f(999e-2)).to_be(10) + - it rounds to specified number of decimal places: + expect(f(1.234, 0)).to_be(1.0) + expect(f(5.678, 0)).to_be(6.0) + expect(f(1.234, 1)).to_be(1.2) + expect(f(5.678, 1)).to_be(5.7) + expect(f(1.234, 2)).to_be(1.23) + expect(f(5.678, 2)).to_be(5.68) + expect(f(9.999, 2)).to_be(10) + expect(f(11111e-2, 3)).to_be(11111e-2) + expect(f(99999e-2, 3)).to_be(99999e-2) + expect(f(11111e-3, 3)).to_be(11111e-3) + expect(f(99999e-3, 3)).to_be(99999e-3) + expect(f(11111e-4, 3)).to_be(1111e-3) + expect(f(99999e-4, 3)).to_be(10) + expect(f(99999e-5, 3)).to_be(1) + - it rounds negative values correctly: + expect(f(-1.234, 0)).to_be(-1.0) + expect(f(-5.678, 0)).to_be(-6.0) + expect(f(-1.234, 1)).to_be(-1.2) + expect(f(-5.678, 1)).to_be(-5.7) + expect(f(-1.234, 2)).to_be(-1.23) + expect(f(-5.678, 2)).to_be(-5.68) + expect(f(-9.999, 2)).to_be(-10) + expect(f(-11111e-2, 3)).to_be(-11111e-2) + expect(f(-99999e-2, 3)).to_be(-99999e-2) + expect(f(-11111e-3, 3)).to_be(-11111e-3) + expect(f(-99999e-3, 3)).to_be(-99999e-3) + expect(f(-11111e-4, 3)).to_be(-1111e-3) + expect(f(-99999e-4, 3)).to_be(-10) + expect(f(-99999e-5, 3)).to_be(-1) diff --git a/Data/BuiltIn/Libraries/lua-stdlib/spec/package_spec.yaml b/Data/BuiltIn/Libraries/lua-stdlib/spec/package_spec.yaml new file mode 100644 index 0000000..23ce961 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-stdlib/spec/package_spec.yaml @@ -0,0 +1,202 @@ +# General Lua Libraries for Lua 5.1, 5.2 & 5.3 +# Copyright (C) 2011-2018 stdlib authors + +before: | + base_module = 'package' + this_module = 'std.package' + global_table = '_G' + + extend_base = {'find', 'insert', 'mappath', 'normalize', 'remove'} + + M = require(this_module) + + path = M.normalize('begin', 'middle', 'end') + + function catfile(...) + return table.concat({...}, M.dirsep) + end + function catpath(...) + return table.concat({...}, M.pathsep) + end + + +specify std.package: +- context when required: + - context by name: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core package table: + expect(show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core package table: + expect(show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of(extend_base) + + - context via the std module: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by='std'}). + to_equal {} + - it does not touch the core package table: + expect(show_apis {added_to=base_module, by='std'}). + to_equal {} + + +- describe find: + - before: | + path = table.concat({'begin', 'm%ddl.', 'end'}, M.pathsep) + + f = M.find + + - context with bad arguments: + badargs.diagnose(f, 'std.package.find(string, string, ?int, ?boolean|:plain)') + + - it returns nil for unmatched element: + expect(f(path, 'unmatchable')).to_be(nil) + - it returns the element index for a matched element: + expect(f(path, 'end')).to_be(3) + - it returns the element text for a matched element: + i, element = f(path, 'e.*n') + expect({i, element}).to_equal {1, 'begin'} + - it accepts a search start element argument: + i, element = f(path, 'e.*n', 2) + expect({i, element}).to_equal {3, 'end'} + - it works with plain text search strings: + expect(f(path, 'm%ddl.')).to_be(nil) + i, element = f(path, '%ddl.', 1, ':plain') + expect({i, element}).to_equal {2, 'm%ddl.'} + + +- describe insert: + - before: + f = M.insert + + - context with bad arguments: + badargs.diagnose(f, 'std.package.insert(string, [int], string)') + + - it appends by default: + expect(f(path, 'new')). + to_be(M.normalize('begin', 'middle', 'end', 'new')) + - it prepends with pos set to 1: + expect(f(path, 1, 'new')). + to_be(M.normalize('new', 'begin', 'middle', 'end')) + - it can insert in the middle too: + expect(f(path, 2, 'new')). + to_be(M.normalize('begin', 'new', 'middle', 'end')) + expect(f(path, 3, 'new')). + to_be(M.normalize('begin', 'middle', 'new', 'end')) + - it normalizes the returned path: + path = table.concat({'begin', 'middle', 'end'}, M.pathsep) + expect(f(path, 'new')). + to_be(M.normalize('begin', 'middle', 'end', 'new')) + expect(f(path, 1, './x/../end')). + to_be(M.normalize('end', 'begin', 'middle')) + + +- describe mappath: + - before: | + expected = require 'std.string'.split(path, M.pathsep) + + f = M.mappath + + - context with bad arguments: + badargs.diagnose(f, 'std.package.mappath(string, function, ?any*)') + + - it calls a function with each path element: + t = {} + f(path, function(e) + t[#t + 1] = e + end) + expect(t).to_equal(expected) + - it passes additional arguments through: | + reversed = {} + for i = #expected, 1, -1 do + table.insert(reversed, expected[i]) + end + t = {} + f(path, function(e, pos) + table.insert(t, pos, e) + end, 1) + expect(t).to_equal(reversed) + + +- describe normalize: + - before: + f = M.normalize + + - context with bad arguments: + badargs.diagnose(f, 'std.package.normalize(string*)') + + - context with a single element: + - it strips redundant . directories: + expect(f './x/./y/.').to_be(catfile('.', 'x', 'y')) + - it strips redundant .. directories: + expect(f '../x/../y/z/..').to_be(catfile('..', 'y')) + expect(f '../x/../y/z/..').to_be(catfile('..', 'y')) + expect(f '../../x/../y/z/..').to_be(catfile('..', '..', 'y')) + expect(f '../../x/../y/./..').to_be(catfile('..', '..')) + expect(f '../../w/x/../../y/z/..').to_be(catfile('..', '..', 'y')) + expect(f '../../w/./../.././z/..').to_be(catfile('..', '..', '..')) + - it leaves leading .. directories unmolested: + expect(f '../../1').to_be(catfile('..', '..', '1')) + expect(f './../../1').to_be(catfile('..', '..', '1')) + - it normalizes / to platform dirsep: + expect(f '/foo/bar').to_be(catfile('', 'foo', 'bar')) + - it normalizes ? to platform pathmark: + expect(f '?.lua'). + to_be(catfile('.', M.pathmark .. '.lua')) + - it strips redundant trailing /: + expect(f '/foo/bar/').to_be(catfile('', 'foo', 'bar')) + - it inserts missing ./ for relative paths: + for _, path in ipairs {'x', './x'} do + expect(f(path)).to_be(catfile('.', 'x')) + end + - context with multiple elements: + - it strips redundant . directories: + expect(f('./x/./y/.', 'x')). + to_be(catpath(catfile('.', 'x', 'y'), catfile('.', 'x'))) + - it strips redundant .. directories: + expect(f('../x/../y/z/..', 'x')). + to_be(catpath(catfile('..', 'y'), catfile('.', 'x'))) + - it normalizes / to platform dirsep: + expect(f('/foo/bar', 'x')). + to_be(catpath(catfile('', 'foo', 'bar'), catfile('.', 'x'))) + - it normalizes ? to platform pathmark: + expect(f('?.lua', 'x')). + to_be(catpath(catfile('.', M.pathmark .. '.lua'), catfile('.', 'x'))) + - it strips redundant trailing /: + expect(f('/foo/bar/', 'x')). + to_be(catpath(catfile('', 'foo', 'bar'), catfile('.', 'x'))) + - it inserts missing ./ for relative paths: + for _, path in ipairs {'x', './x'} do + expect(f(path, 'a')). + to_be(catpath(catfile('.', 'x'), catfile('.', 'a'))) + end + - it eliminates all but the first equivalent elements: + expect(f(catpath('1', 'x', '2', './x', './2', './x/../x'))). + to_be(catpath('./1', './x', './2')) + + +- describe remove: + - before: + f = M.remove + + - context with bad arguments: + badargs.diagnose(f, 'std.package.remove(string, ?int)') + + - it removes the last item by default: + expect(f(path)).to_be(M.normalize('begin', 'middle')) + - it pops the first item with pos set to 1: + expect(f(path, 1)).to_be(M.normalize('middle', 'end')) + - it can remove from the middle too: + expect(f(path, 2)).to_be(M.normalize('begin', 'end')) + - it does not normalize the returned path: + path = table.concat({'begin', 'middle', 'end'}, M.pathsep) + expect(f(path)). + to_be(table.concat({'begin', 'middle'}, M.pathsep)) + + +- it splits package.config up: + expect(string.format('%s\n%s\n%s\n%s\n%s\n', + M.dirsep, M.pathsep, M.pathmark, M.execdir, M.igmark) + ).to_contain(package.config) diff --git a/Data/BuiltIn/Libraries/lua-stdlib/spec/spec_helper.lua b/Data/BuiltIn/Libraries/lua-stdlib/spec/spec_helper.lua new file mode 100644 index 0000000..642712f --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-stdlib/spec/spec_helper.lua @@ -0,0 +1,416 @@ +--[[ + General Lua Libraries for Lua 5.1, 5.2 & 5.3 + Copyright (C) 2011-2018 stdlib authors +]] + +local typecheck +have_typecheck, typecheck = pcall(require, 'typecheck') + +local inprocess = require 'specl.inprocess' +local hell = require 'specl.shell' +local std = require 'specl.std' + +badargs = require 'specl.badargs' + + +local top_srcdir = os.getenv 'top_srcdir' or '.' +local top_builddir = os.getenv 'top_builddir' or '.' + +package.path = std.package.normalize( + top_builddir .. '/lib/?.lua', + top_builddir .. '/lib/?/init.lua', + top_srcdir .. '/lib/?.lua', + top_srcdir .. '/lib/?/init.lua', + package.path +) + + +-- Allow user override of LUA binary used by hell.spawn, falling +-- back to environment PATH search for 'lua' if nothing else works. +local LUA = os.getenv 'LUA' or 'lua' + + +-- Simplified version for specifications, does not support functable +-- valued __len metamethod, so don't write examples that need that! +function len(x) + local __len = getmetatable(x) or {} + if type(__len) == 'function' then + return __len(x) + end + if type(x) ~= 'table' then + return #x + end + + local n = #x + for i = 1, n do + if x[i] == nil then + return i -1 + end + end + return n +end + + +-- Make sure we have a maxn even when _VERSION ~= 5.1 +-- @fixme remove this when we get unpack from specl.std +maxn = table.maxn or function(t) + local n = 0 + for k in pairs(t) do + if type(k) == 'number' and k > n then + n = k + end + end + return n +end + + +pack = table.pack or function(...) + return {n=select('#', ...), ...} +end + + +-- Take care to always unpack upto the highest numeric index, for +-- consistency across Lua versions. +local _unpack = table.unpack or unpack + +-- @fixme pick this up from specl.std with the next release +function unpack(t, i, j) + return _unpack(t, tonumber(i) or 1, tonumber(j or t.n or len(t))) +end + + +-- In case we're not using a bleeding edge release of Specl... +_diagnose = badargs.diagnose +badargs.diagnose = function(...) + if have_typecheck then + return _diagnose(...) + end +end + +badargs.result = badargs.result or function(fname, i, want, got) + if want == nil then -- numbers only for narg error + i, want = i - 1, i + end + + if got == nil and type(want) == 'number' then + local s = "bad result #%d from '%s'(no more than %d result%s expected, got %d)" + return s:format(i + 1, fname, i, i == 1 and '' or 's', want) + end + + local function showarg(s) + return('|' .. s .. '|'): + gsub('|%?', '|nil|'): + gsub('|nil|', '|no value|'): + gsub('|any|', '|any value|'): + gsub('|#', '|non-empty '): + gsub('|func|', '|function|'): + gsub('|file|', '|FILE*|'): + gsub('^|', ''): + gsub('|$', ''): + gsub('|([^|]+)$', 'or %1'): + gsub('|', ', ') + end + + return string.format("bad result #%d from '%s'(%s expected, got %s)", + i, fname, showarg(want), got or 'no value') +end + + +-- Wrap up badargs function in a succinct single call. +function init(M, mname, fname) + local name =(mname .. '.' .. fname):gsub('^%.', '') + return M[fname], + function(...) + return badargs.format(name, ...) + end, + function(...) + return badargs.result(name, ...) + end +end + + +-- A copy of base.lua:type, so that an unloadable base.lua doesn't +-- prevent everything else from working. +function objtype(o) + return(getmetatable(o) or {})._type or io.type(o) or type(o) +end + + +function nop() end + + +-- Error message specifications use this to shorten argument lists. +-- Copied from functional.lua to avoid breaking all tests if functional +-- cannot be loaded correctly. +function bind(f, fix) + return function(...) + local arg = {} + for i, v in pairs(fix) do + arg[i] = v + end + local i = 1 + for _, v in pairs {...} do + while arg[i] ~= nil do + i = i + 1 + end + arg[i] = v + end + return f(unpack(arg)) + end +end + + +local function mkscript(code) + local f = os.tmpname() + local h = io.open(f, 'w') + h:write(code) + h:close() + return f +end + + +--- Run some Lua code with the given arguments and input. +-- @string code valid Lua code +-- @tparam[opt={}] string|table arg single argument, or table of +-- arguments for the script invocation. +-- @string[opt] stdin standard input contents for the script process +-- @treturn specl.shell.Process|nil status of resulting process if +-- execution was successful, otherwise nil +function luaproc(code, arg, stdin) + local f = mkscript(code) + if type(arg) ~= 'table' then + arg = {arg} + end + local cmd = {LUA, f, unpack(arg)} + -- inject env and stdin keys separately to avoid truncating `...` in + -- cmd constructor + cmd.env = {LUA_PATH=package.path, LUA_INIT='', LUA_INIT_5_2=''} + cmd.stdin = stdin + local proc = hell.spawn(cmd) + os.remove(f) + return proc +end + + +--- Check deprecation output when calling a named function in the given module. +-- Note that the script fragments passed in *argstr* and *objectinit* +-- can reference the module table as `M`, and(where it would make sense) +-- an object prototype as `P` and instance as `obj`. +-- @param deprecate value of `std._debug.deprecate` +-- @string module dot delimited module path to load +-- @string fname name of a function in the table returned by requiring *module* +-- @param[opt=''] args arguments to pass to *fname* call, must be stringifiable +-- @string[opt=nil] objectinit object initializer to instantiate an +-- object for object method deprecation check +-- @treturn specl.shell.Process|nil status of resulting process if +-- execution was successful, otherwise nil +function deprecation(deprecate, module, fname, args, objectinit) + args = args or '' + local script + if objectinit == nil then + script = string.format([[ + require 'std._debug'.deprecate = %s + M = require '%s' + P = M.prototype + print(M.%s(%s)) + ]], tostring(deprecate), module, fname, tostring(args)) + else + script = string.format([[ + require 'std._debug'.deprecate = %s + local M = require '%s' + local P = M.prototype + local obj = P(%s) + print(obj:%s(%s)) + ]], tostring(deprecate), module, objectinit, fname, tostring(args)) + end + return luaproc(script) +end + + +--- Concatenate the contents of listed existing files. +-- @string ... names of existing files +-- @treturn string concatenated contents of those files +function concat_file_content(...) + local t = {} + for _, name in ipairs {...} do + h = io.open(name) + t[#t + 1] = h:read '*a' + end + return table.concat(t) +end + + +local function tabulate_output(code) + local proc = luaproc(code) + if proc.status ~= 0 then + return error(proc.errout) + end + local r = {} + proc.output:gsub('(%S*)[%s]*', + function(x) + if x ~= '' then + r[x] = true + end + end) + return r +end + + +--- Show changes to tables wrought by a require statement. +-- There are a few modes to this function, controlled by what named +-- arguments are given. Lists new keys in T1 after `require 'import'`: +-- +-- show_apis {added_to=T1, by=import} +-- +-- List keys returned from `require 'import'`, which have the same +-- value in T1: +-- +-- show_apis {from=T1, used_by=import} +-- +-- List keys from `require 'import'`, which are also in T1 but with +-- a different value: +-- +-- show_apis {from=T1, enhanced_by=import} +-- +-- List keys from T2, which are also in T1 but with a different value: +-- +-- show_apis {from=T1, enhanced_in=T2} +-- +-- @tparam table argt one of the combinations above +-- @treturn table a list of keys according to criteria above +function show_apis(argt) + local added_to, from, not_in, enhanced_in, enhanced_after, by = + argt.added_to, argt.from, argt.not_in, argt.enhanced_in, + argt.enhanced_after, argt.by + + if added_to and by then + return tabulate_output([[ + local before, after = {}, {} + for k in pairs(]] .. added_to .. [[) do + before[k] = true + end + + local M = require ']] .. by .. [[' + for k in pairs(]] .. added_to .. [[) do + after[k] = true + end + + for k in pairs(after) do + if not before[k] then + print(k) + end + end + ]]) + + elseif from and not_in then + return tabulate_output([[ + local _ENV = require 'std.normalize' { + from = ']] .. from .. [[', + M = require ']] .. not_in .. [[', + } + + for k in pairs(M) do + -- M[1] is typically the module namespace name, don't match + -- that! + if k ~= 1 and from[k] ~= M[k] then + print(k) + end + end + ]]) + + elseif from and enhanced_in then + return tabulate_output([[ + local _ENV = require 'std.normalize' { + from = ']] .. from .. [[', + M = require ']] .. enhanced_in .. [[', + } + + for k, v in pairs(M) do + if from[k] ~= M[k] and from[k] ~= nil then + print(k) + end + end + ]]) + + elseif from and enhanced_after then + return tabulate_output([[ + local _ENV = require 'std.normalize' { + from = ']] .. from .. [[', + } + local before, after = {}, {} + for k, v in pairs(from) do + before[k] = v + end + ]] .. enhanced_after .. [[ + for k, v in pairs(from) do + after[k] = v + end + + for k, v in pairs(before) do + if after[k] ~= nil and after[k] ~= v then + print(k) + end + end + ]]) + end + + assert(false, 'missing argument to show_apis') +end + + +-- Stub inprocess.capture if necessary; new in Specl 12. +capture = inprocess.capture or function(f, arg) + return nil, nil, f(unpack(arg or {})) +end + + +do + -- Custom matcher for set size and set membership. + + local util = require 'specl.util' + local matchers = require 'specl.matchers' + + local Matcher, matchers, q = + matchers.Matcher, matchers.matchers, matchers.stringify + + matchers.have_size = Matcher { + function(self, actual, expect) + local size = 0 + for _ in pairs(actual) do + size = size + 1 + end + return size == expect + end, + + actual = 'table', + + format_expect = function(self, expect) + return ' a table containing ' .. expect .. ' elements, ' + end, + + format_any_of = function(self, alternatives) + return ' a table with any of ' .. + util.concat(alternatives, util.QUOTED) .. ' elements, ' + end, + } + + matchers.have_member = Matcher { + function(self, actual, expect) + return actual[expect] ~= nil + end, + + actual = 'set', + + format_expect = function(self, expect) + return ' a set containing ' .. q(expect) .. ', ' + end, + + format_any_of = function(self, alternatives) + return ' a set containing any of ' .. + util.concat(alternatives, util.QUOTED) .. ', ' + end, + } + + -- Alias that doesn't tickle sc_error_message_uppercase. + matchers.raise = matchers.error +end diff --git a/Data/BuiltIn/Libraries/lua-stdlib/spec/std_spec.yaml b/Data/BuiltIn/Libraries/lua-stdlib/spec/std_spec.yaml new file mode 100644 index 0000000..b747abf --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-stdlib/spec/std_spec.yaml @@ -0,0 +1,444 @@ +# General Lua Libraries for Lua 5.1, 5.2 & 5.3 +# Copyright (C) 2011-2018 stdlib authors + +before: | + this_module = 'std' + global_table = '_G' + + exported_apis = {'assert', 'elems', 'eval', 'getmetamethod', + 'ielems', 'ipairs', 'npairs', 'pairs', + 'require', 'ripairs', 'rnpairs'} + + -- Tables with iterator metamethods used by various examples. + __pairs = setmetatable({content='a string'}, { + __pairs = function(t) + return function(x, n) + if n < #x.content then + return n+1, string.sub(x.content, n+1, n+1) + end + end, t, 0 + end, + }) + __index = setmetatable({content='a string'}, { + __index = function(t, n) + if n <= #t.content then + return t.content:sub(n, n) + end + end, + __len = function(t) + return #t.content + end, + }) + + M = require(this_module) + M.version = nil -- previous specs may have autoloaded it + + +specify std: +- context when required: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it exports the documented apis: + t = {} + for k in pairs(M) do + t[#t + 1] = k + end + expect(t).to_contain.a_permutation_of(exported_apis) + +- context when lazy loading: + - it has no submodules on initial load: + for _, v in pairs(M) do + expect(type(v)).not_to_be 'table' + end + - it loads submodules on demand: + lazy = M.math + expect(lazy).to_be(require 'std.math') + - it loads submodule functions on demand: + expect(M.math.round(3.141592)).to_be(3) + +- describe assert: + - before: + f = M.assert + + - context with bad arguments: + badargs.diagnose(f, 'std.assert(?any, ?string, ?any*)') + + - context when it does not trigger: + - it has a truthy initial argument: + expect(f(1)).not_to_raise 'any error' + expect(f(true)).not_to_raise 'any error' + expect(f 'yes').not_to_raise 'any error' + expect(f(false == false)).not_to_raise 'any error' + - it returns the initial argument: + expect(f(1)).to_be(1) + expect(f(true)).to_be(true) + expect(f 'yes').to_be 'yes' + expect(f(false == false)).to_be(true) + - context when it triggers: + - it has a falsey initial argument: + expect(f()).to_raise() + expect(f(false)).to_raise() + expect(f(1 == 0)).to_raise() + - it throws an optional error string: + expect(f(false, 'ah boo')).to_raise 'ah boo' + - it plugs specifiers with string.format: | + expect(f(nil, '%s %d: %q', 'here', 42, 'a string')). + to_raise(string.format('%s %d: %q', 'here', 42, 'a string')) + + +- describe elems: + - before: + f = M.elems + + - context with bad arguments: + badargs.diagnose(f, 'std.elems(table)') + + - it is an iterator over table values: + t = {} + for e in f {'foo', bar='baz', 42} do + t[#t + 1] = e + end + expect(t).to_contain.a_permutation_of {'foo', 'baz', 42} + - it respects __pairs metamethod: | + t = {} + for v in f(__pairs) do + t[#t + 1] = v + end + expect(t). + to_contain.a_permutation_of {'a', ' ', 's', 't', 'r', 'i', 'n', 'g'} + - it works for an empty list: + t = {} + for e in f {} do + t[#t + 1] = e + end + expect(t).to_equal {} + + +- describe eval: + - before: + f = M.eval + + - context with bad arguments: + badargs.diagnose(f, 'std.eval(string)') + + - it diagnoses invalid lua: + # Some internal error when eval tries to call uncompilable '=' code. + expect(f '=').to_raise() + - it evaluates a string of lua code: + expect(f 'math.min(2, 10)').to_be(math.min(2, 10)) + + +- describe getmetamethod: + - before: + f = M.getmetamethod + + - context with bad arguments: + badargs.diagnose(f, 'std.getmetamethod(?any, string)') + + - context with a table: + - before: + method = function() + return 'called' + end + functor = setmetatable({}, {__call=method}) + t = setmetatable({}, { + _type='table', _method=method, _functor=functor, + }) + - it returns nil for missing metamethods: + expect(f(t, 'not a metamethod on t')).to_be(nil) + - it returns nil for non-callable metatable entries: + expect(f(t, '_type')).to_be(nil) + - it returns a method from the metatable: + expect(f(t, '_method')).to_be(method) + expect(f(t, '_method')()).to_be 'called' + - it returns a functor from the metatable: + expect(f(t, '_functor')).to_be(functor) + expect(f(t, '_functor')()).to_be 'called' + + +- describe ielems: + - before: + f = M.ielems + + - context with bad arguments: + badargs.diagnose(f, 'std.ielems(table)') + + - it is an iterator over integer-keyed table values: + t = {} + for e in f {'foo', 42} do + t[#t + 1] = e + end + expect(t).to_equal {'foo', 42} + - it ignores the dictionary part of a table: + t = {} + for e in f {'foo', 42; bar='baz', qux='quux'} do + t[#t + 1] = e + end + expect(t).to_equal {'foo', 42} + - it respects __len metamethod: + t = {} + for v in f(__index) do + t[#t + 1] = v + end + expect(t).to_equal {'a', ' ', 's', 't', 'r', 'i', 'n', 'g'} + - it works for an empty list: + t = {} + for e in f {} do + t[#t + 1] = e + end + expect(t).to_equal {} + + +- describe ipairs: + - before: + f = M.ipairs + + - context with bad arguments: + badargs.diagnose(f, 'std.ipairs(table)') + + - it is an iterator over integer-keyed table values: + t = {} + for i, v in f {'foo', 42} do + t[i] = v + end + expect(t).to_equal {'foo', 42} + - it ignores the dictionary part of a table: + t = {} + for i, v in f {'foo', 42; bar='baz', qux='quux'} do + t[i] = v + end + expect(t).to_equal {'foo', 42} + - it respects __len metamethod: + t = {} + for k, v in f(__index) do + t[k] = v + end + expect(t).to_equal {'a', ' ', 's', 't', 'r', 'i', 'n', 'g'} + - it works for an empty list: + t = {} + for i, v in f {} do + t[i] = v + end + expect(t).to_equal {} + + +- describe npairs: + - before: + f = M.npairs + + - context with bad arguments: + badargs.diagnose(f, 'std.npairs(table)') + + - it is an iterator over integer-keyed table values: + t = {} + for i, v in f {'foo', 42, nil, nil, 'five'} do + t[i] = v + end + expect(t).to_equal {'foo', 42, nil, nil, 'five'} + - it ignores the dictionary part of a table: + t = {} + for i, v in f {'foo', 42, nil, nil, 'five'; bar='baz', qux='quux'} do + t[i] = v + end + expect(t).to_equal {'foo', 42, nil, nil, 'five'} + - it respects __len metamethod: + t = {} + for _, v in f(setmetatable({[2]=false}, {__len=function(self) return 4 end})) do + t[#t + 1] = tostring(v) + end + expect(table.concat(t, ',')).to_be 'nil,false,nil,nil' + - it works for an empty list: + t = {} + for i, v in f {} do + t[i] = v + end + expect(t).to_equal {} + + +- describe pairs: + - before: + f = M.pairs + + - context with bad arguments: + badargs.diagnose(f, 'std.pairs(table)') + + - it is an iterator over all table values: + t = {} + for k, v in f {'foo', bar='baz', 42} do + t[k] = v + end + expect(t).to_equal {'foo', bar='baz', 42} + - it respects __pairs metamethod: | + t = {} + for k, v in f(__pairs) do + t[k] = v + end + expect(t). + to_contain.a_permutation_of {'a', ' ', 's', 't', 'r', 'i', 'n', 'g'} + - it works for an empty list: + t = {} + for k, v in f {} do + t[k] = v + end + expect(t).to_equal {} + + +- describe require: + - before: + f = M.require + + - context with bad arguments: + badargs.diagnose(f, 'std.require(string, ?string, ?string, ?string)') + + - it diagnoses non-existent module: + expect(f('module-not-exists', '', '')).to_raise 'module-not-exists' + - it diagnoses module too old: + expect(f('std', '9999', '9999')). + to_raise "require 'std' with at least version 9999," + - it diagnoses module too new: + expect(f('std', '0', '0')). + to_raise "require 'std' with version less than 0," + - context when the module version is compatible: + - it returns the module table: + expect(f('std', '0', '9999')).to_be(require 'std') + - it places no upper bound by default: + expect(f('std', '0')).to_be(require 'std') + - it places no lower bound by default: + expect(f 'std').to_be(require 'std') + - it uses _VERSION when version field is nil: + expect(luaproc [[ + package.loaded['poop'] = {_VERSION='41.1'} + f = require 'std'.require + print(f('poop', '41', '9999')._VERSION) + ]]).to_succeed_with '41.1\n' + - context with semantic versioning: + - before: + std = require 'std' + ver = std.version + std.version = '1.2.3' + - after: + std.version = ver + - it diagnoses module too old: + expect(f('std', '1.2.4')). + to_raise "require 'std' with at least version 1.2.4," + expect(f('std', '1.3')). + to_raise "require 'std' with at least version 1.3," + expect(f('std', '2.1.2')). + to_raise "require 'std' with at least version 2.1.2," + expect(f('std', '2')). + to_raise "require 'std' with at least version 2," + expect(f('std', '1.2.10')). + to_raise "require 'std' with at least version 1.2.10," + - it diagnoses module too new: + expect(f('std', nil, '1.2.2')). + to_raise "require 'std' with version less than 1.2.2," + expect(f('std', nil, '1.1')). + to_raise "require 'std' with version less than 1.1," + expect(f('std', nil, '1.1.2')). + to_raise "require 'std' with version less than 1.1.2," + expect(f('std', nil, '1')). + to_raise "require 'std' with version less than 1," + - it returns modules with version in range: + expect(f('std')).to_be(std) + expect(f('std', '1')).to_be(std) + expect(f('std', '1.2.3')).to_be(std) + expect(f('std', nil, '2')).to_be(std) + expect(f('std', nil, '1.3')).to_be(std) + expect(f('std', nil, '1.2.10')).to_be(std) + expect(f('std', '1.2.3', '1.2.4')).to_be(std) + - context with several numbers in version string: + - before: + std = require 'std' + ver = std.version + std.version = 'standard library for Lua 5.3 / 41.0.0' + - after: + std.version = ver + - it diagnoses module too old: + expect(f('std', '42')).to_raise() + - it diagnoses module too new: + expect(f('std', nil, '40')).to_raise() + - it returns modules with version in range: + expect(f('std')).to_be(std) + expect(f('std', '1')).to_be(std) + expect(f('std', '41')).to_be(std) + expect(f('std', nil, '42')).to_be(std) + expect(f('std', '41', '42')).to_be(std) + + +- describe ripairs: + - before: + f = M.ripairs + + - context with bad arguments: + badargs.diagnose(f, 'std.ripairs(table)') + + - it returns a function, the table and a number: + fn, t, i = f {1, 2, 3} + expect({type(fn), t, type(i)}).to_equal {'function', {1, 2, 3}, 'number'} + - it iterates over the array part of a table: + t, u = {1, 2, 3; a=4, b=5, c=6}, {} + for i, v in f(t) do + u[i] = v + end + expect(u).to_equal {1, 2, 3} + - it returns elements in reverse order: + t, u = {'one', 'two', 'five'}, {} + for _, v in f(t) do + u[#u + 1] = v + end + expect(u).to_equal {'five', 'two', 'one'} + - it respects __len metamethod: + t = {} + for i, v in f(__index) do + t[i] = v + end + expect(t).to_equal {'a', ' ', 's', 't', 'r', 'i', 'n', 'g'} + t = {} + for _, v in f(__index) do + t[#t + 1] = v + end + expect(t).to_equal {'g', 'n', 'i', 'r', 't', 's', ' ', 'a'} + - it works with the empty list: + t = {} + for k, v in f {} do + t[k] = v + end + expect(t).to_equal {} + + +- describe rnpairs: + - before: + f = M.rnpairs + + - context with bad arguments: + badargs.diagnose(f, 'std.rnpairs(table)') + + - it returns a function, the table and a number: + fn, t, i = f {1, 2, nil, nil, 3} + expect({type(fn), t, type(i)}). + to_equal {'function', {1, 2, nil, nil, 3}, 'number'} + - it iterates over the array part of a table: + t, u = {1, 2, nil, nil, 3; a=4, b=5, c=6}, {} + for i, v in f(t) do + u[i] = v + end + expect(u).to_equal {1, 2, nil, nil, 3} + - it returns elements in reverse order: + t, u, i = {'one', 'two', nil, nil, 'five'}, {}, 1 + for _, v in f(t) do + u[i], i = v, i + 1 + end + expect(u).to_equal {'five', nil, nil, 'two', 'one'} + - it respects __len metamethod: + t = {} + for _, v in f(setmetatable({[2]=false}, {__len=function(self) return 4 end})) do + t[#t + 1] = tostring(v) + end + expect(table.concat(t, ',')).to_be 'nil,nil,false,nil' + - it works with the empty list: + t = {} + for k, v in f {} do + t[k] = v + end + expect(t).to_equal {} diff --git a/Data/BuiltIn/Libraries/lua-stdlib/spec/string_spec.yaml b/Data/BuiltIn/Libraries/lua-stdlib/spec/string_spec.yaml new file mode 100644 index 0000000..2fa47f2 --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-stdlib/spec/string_spec.yaml @@ -0,0 +1,549 @@ +# General Lua Libraries for Lua 5.1, 5.2 & 5.3 +# Copyright (C) 2011-2018 stdlib authors + +before: + base_module = 'string' + this_module = 'std.string' + global_table = '_G' + + extend_base = {'__concat', '__index', + 'caps', 'chomp', 'escape_pattern', 'escape_shell', + 'finds', 'format', 'ltrim', + 'numbertosi', 'ordinal_suffix', 'pad', + 'prettytostring', 'rtrim', 'split', + 'tfind', 'trim', 'wrap'} + + M = require(this_module) + getmetatable('').__concat = M.__concat + getmetatable('').__index = M.__index + +specify std.string: +- before: + subject = 'a string \n\n' + +- context when required: + - context by name: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core string table: + expect(show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core string table: + expect(show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of(extend_base) + + - context via the std module: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by='std'}). + to_equal {} + - it does not touch the core string table: + expect(show_apis {added_to=base_module, by='std'}). + to_equal {} + +- describe ..: + - it concatenates string arguments: + target = 'a string \n\n another string' + expect(subject .. ' another string').to_be(target) + - it stringifies non-string arguments: + argument = {'a table'} + expect(subject .. argument).to_be(subject .. '{1="a table"}') + - it stringifies nil arguments: + argument = nil + expect(subject .. argument). + to_be(string.format('%s%s', subject, require 'std.normalize'.str(argument))) + - it does not perturb the original subject: + original = subject + newstring = subject .. ' concatenate something' + expect(subject).to_be(original) + + +- describe caps: + - before: + f = M.caps + + - context with bad arguments: + badargs.diagnose(f, 'std.string.caps(string)') + + - it capitalises words of a string: + target = 'A String \n\n' + expect(f(subject)).to_be(target) + - it changes only the first letter of each word: + expect(f 'a stRiNg').to_be 'A StRiNg' + - it is available as a string metamethod: + expect(('a stRiNg'):caps()).to_be 'A StRiNg' + - it does not perturb the original subject: + original = subject + newstring = f(subject) + expect(subject).to_be(original) + + +- describe chomp: + - before: + target = 'a string \n' + f = M.chomp + + - context with bad arguments: + badargs.diagnose(f, 'std.string.chomp(string)') + + - it removes a single trailing newline from a string: + expect(f(subject)).to_be(target) + - it does not change a string with no trailing newline: + subject = 'a string ' + expect(f(subject)).to_be(subject) + - it is available as a string metamethod: + expect(subject:chomp()).to_be(target) + - it does not perturb the original subject: + original = subject + newstring = f(subject) + expect(subject).to_be(original) + + +- describe escape_pattern: + - before: + magic = {} + meta = '^$()%.[]*+-?' + for i = 1, string.len(meta) do + magic[meta:sub(i, i)] = true + end + f = M.escape_pattern + + - context with bad arguments: + badargs.diagnose(f, 'std.string.escape_pattern(string)') + + - context with each printable ASCII char: + - before: + subject, target = '', '' + for c = 32, 126 do + s = string.char(c) + subject = subject .. s + if magic[s] then + target = target .. '%' + end + target = target .. s + end + - 'it inserts a % before any non-alphanumeric in a string': + expect(f(subject)).to_be(target) + - it is available as a string metamethod: + expect(subject:escape_pattern()).to_be(target) + - it does not perturb the original subject: + original = subject + newstring = f(subject) + expect(subject).to_be(original) + + +- describe escape_shell: + - before: + f = M.escape_shell + + - context with bad arguments: + badargs.diagnose(f, 'std.string.escape_shell(string)') + + - context with each printable ASCII char: + - before: + subject, target = '', '' + for c = 32, 126 do + s = string.char(c) + subject = subject .. s + if s:match('[][ ()\\\'"]') then + target = target .. '\\' + end + target = target .. s + end + - 'it inserts a \\ before any shell metacharacters': + expect(f(subject)).to_be(target) + - it is available as a string metamethod: + expect(subject:escape_shell()).to_be(target) + - it does not perturb the original subject: + original = subject + newstring = f(subject) + expect(subject).to_be(original) + - 'it diagnoses non-string arguments': + if typecheck then + expect(f()).to_raise('string expected') + expect(f {'a table'}).to_raise('string expected') + end + + +- describe finds: + - before: + subject = 'abcd' + f = M.finds + + - context with bad arguments: + badargs.diagnose(f, 'std.string.finds(string, string, ?int, ?boolean|:plain)') + + - context given a complex nested list: + - before: + target = {{1, 2; capt={'a', 'b'}}, {3, 4; capt={'c', 'd'}}} + - it creates a list of pattern captures: + expect({f(subject, '(.)(.)')}).to_equal({target}) + - it is available as a string metamethod: + expect({subject:finds('(.)(.)')}).to_equal({target}) + - it creates an empty list where no captures are matched: + target = {} + expect({f(subject, '(x)')}).to_equal({target}) + - it creates an empty list for a pattern without captures: + target = {{1, 1; capt={}}} + expect({f(subject, 'a')}).to_equal({target}) + - it starts the search at a specified index into the subject: + target = {{8, 9; capt={'a', 'b'}}, {10, 11; capt={'c', 'd'}}} + expect({f('garbage' .. subject, '(.)(.)', 8)}).to_equal({target}) + - it does not perturb the original subject: + original = subject + newstring = f(subject, '...') + expect(subject).to_be(original) + + +- describe format: + - before: + subject = 'string=%s, number=%d' + + f = M.format + + - context with bad arguments: + badargs.diagnose(f, 'std.string.format(string, ?any*)') + + - it returns a single argument without attempting formatting: + expect(f(subject)).to_be(subject) + - it is available as a string metamethod: + expect(subject:format()).to_be(subject) + - it does not perturb the original subject: + original = subject + newstring = f(subject) + expect(subject).to_be(original) + + +- describe ltrim: + - before: + subject = ' \t\r\n a short string \t\r\n ' + + f = M.ltrim + + - context with bad arguments: + badargs.diagnose(f, 'std.string.ltrim(string, ?string)') + + - it removes whitespace from the start of a string: + target = 'a short string \t\r\n ' + expect(f(subject)).to_equal(target) + - it supports custom removal patterns: + target = '\r\n a short string \t\r\n ' + expect(f(subject, '[ \t\n]+')).to_equal(target) + - it is available as a string metamethod: + target = '\r\n a short string \t\r\n ' + expect(subject:ltrim('[ \t\n]+')).to_equal(target) + - it does not perturb the original subject: + original = subject + newstring = f(subject, '%W') + expect(subject).to_be(original) + + +- describe numbertosi: + - before: + f = M.numbertosi + + - context with bad arguments: + badargs.diagnose(f, 'std.string.numbertosi(number|string)') + + - it returns a number using SI suffixes: + target = {'1e-9', '1y', '1z', '1a', '1f', '1p', '1n', '1mu', '1m', '1', + '1k', '1M', '1G', '1T', '1P', '1E', '1Z', '1Y', '1e9'} + subject = {} + for n = -28, 28, 3 do + m = 10 *(10 ^ n) + table.insert(subject, f(m)) + end + expect(subject).to_equal(target) + - it coerces string arguments to a number: + expect(f '1000').to_be '1k' + + +- describe ordinal_suffix: + - before: + f = M.ordinal_suffix + + - context with bad arguments: + badargs.diagnose(f, 'std.string.ordinal_suffix(int|string)') + + - it returns the English suffix for a number: + subject, target = {}, {} + for n = -120, 120 do + suffix = 'th' + m = math.abs(n) % 10 + if m == 1 and math.abs(n) % 100 ~= 11 then + suffix = 'st' + elseif m == 2 and math.abs(n) % 100 ~= 12 then + suffix = 'nd' + elseif m == 3 and math.abs(n) % 100 ~= 13 then + suffix = 'rd' + end + table.insert(target, n .. suffix) + table.insert(subject, n .. f(n)) + end + expect(subject).to_equal(target) + - it coerces string arguments to a number: + expect(f '-91').to_be 'st' + + +- describe pad: + - before: + width = 20 + + f = M.pad + + - context with bad arguments: + badargs.diagnose(f, 'std.string.pad(string, int, ?string)') + + - context when string is shorter than given width: + - before: + subject = 'short string' + - it right pads a string to the given width with spaces: + target = 'short string ' + expect(f(subject, width)).to_be(target) + - it left pads a string to the given negative width with spaces: + width = -width + target = ' short string' + expect(f(subject, width)).to_be(target) + - it is available as a string metamethod: + target = 'short string ' + expect(subject:pad(width)).to_be(target) + + - context when string is longer than given width: + - before: + subject = "a string that's longer than twenty characters" + - it truncates a string to the given width: + target = "a string that's long" + expect(f(subject, width)).to_be(target) + - it left pads a string to given width with spaces: + width = -width + target = 'an twenty characters' + expect(f(subject, width)).to_be(target) + - it is available as a string metamethod: + target = "a string that's long" + expect(subject:pad(width)).to_be(target) + + - it does not perturb the original subject: + original = subject + newstring = f(subject, width) + expect(subject).to_be(original) + + +- describe prettytostring: + - before: + f = M.prettytostring + + - context with bad arguments: + badargs.diagnose(f, 'std.string.prettytostring(?any, ?string, ?string)') + + - it renders nil exactly like system tostring: + expect(f(nil)).to_be(tostring(nil)) + - it renders booleans exactly like system tostring: + expect(f(true)).to_be(tostring(true)) + expect(f(false)).to_be(tostring(false)) + - it renders numbers exactly like system tostring: + n = 8723643 + expect(f(n)).to_be(tostring(n)) + - it renders functions exactly like system tostring: + expect(f(f)).to_be(tostring(f)) + - it renders strings with format '%q' styling: + s = 'a string' + expect(f(s)).to_be(string.format('%q', s)) + - it renders empty tables as a pair of braces: + expect(f {}).to_be('{\n}') + - it renders an array prettily: + a = {'one', 'two', 'three'} + expect(f(a, '')). + to_be '{\n[1] = "one",\n[2] = "two",\n[3] = "three",\n}' + - it renders a table prettily: + t = {one=true, two=2, three={3}} + expect(f(t, '')). + to_be '{\none = true,\nthree =\n{\n[1] = 3,\n},\ntwo = 2,\n}' + - it renders table keys in table.sort order: + t = {one=3, two=5, three=4, four=2, five=1} + expect(f(t, '')). + to_be '{\nfive = 1,\nfour = 2,\none = 3,\nthree = 4,\ntwo = 5,\n}' + - it renders keys with invalid symbol names in long hand: + t = {_=0, word=0, ['?']=1, ['a-key']=1, ['[]']=1} + expect(f(t, '')). + to_be '{\n["?"] = 1,\n["[]"] = 1,\n_ = 0,\n["a-key"] = 1,\nword = 0,\n}' + + +- describe rtrim: + - before: + subject = ' \t\r\n a short string \t\r\n ' + + f = M.rtrim + + - context with bad arguments: + badargs.diagnose(f, 'std.string.rtrim(string, ?string)') + + - it removes whitespace from the end of a string: + target = ' \t\r\n a short string' + expect(f(subject)).to_equal(target) + - it supports custom removal patterns: + target = ' \t\r\n a short string \t\r' + expect(f(subject, '[ \t\n]+')).to_equal(target) + - it is available as a string metamethod: + target = ' \t\r\n a short string \t\r' + expect(subject:rtrim('[ \t\n]+')).to_equal(target) + - it does not perturb the original subject: + original = subject + newstring = f(subject, '%W') + expect(subject).to_be(original) + + +- describe split: + - before: + target = {'first', 'the second one', 'final entry'} + subject = table.concat(target, ', ') + + f = M.split + + - context with bad arguments: + badargs.diagnose(f, 'std.string.split(string, ?string)') + + - it falls back to '%s+' when no pattern is given: + expect(f(subject)). + to_equal {'first,', 'the', 'second', 'one,', 'final', 'entry'} + - it returns a one-element list for an empty string: + expect(f('', ', ')).to_equal {''} + - it makes a table of substrings delimited by a separator: + expect(f(subject, ', ')).to_equal(target) + - it returns n+1 elements for n separators: + expect(f(subject, 'zero')).to_have_size(1) + expect(f(subject, 'c')).to_have_size(2) + expect(f(subject, 's')).to_have_size(3) + expect(f(subject, 't')).to_have_size(4) + expect(f(subject, 'e')).to_have_size(5) + - it returns an empty string element for consecutive separators: + expect(f('xyzyzxy', 'yz')).to_equal {'x', '', 'xy'} + - it returns an empty string element when starting with separator: + expect(f('xyzyzxy', 'xyz')).to_equal {'', 'yzxy'} + - it returns an empty string element when ending with separator: + expect(f('xyzyzxy', 'zxy')).to_equal {'xyzy', ''} + - it returns a table of 1-character strings for '' separator: + expect(f('abcdef', '')).to_equal {'', 'a', 'b', 'c', 'd', 'e', 'f', ''} + - it is available as a string metamethod: + expect(subject:split ', ').to_equal(target) + expect(('/foo/bar/baz.quux'):split '/'). + to_equal {'', 'foo', 'bar', 'baz.quux'} + - it does not perturb the original subject: + original = subject + newstring = f(subject, 'e') + expect(subject).to_be(original) + - it takes a Lua pattern as a separator: + expect(f(subject, '%s+')). + to_equal {'first,', 'the', 'second', 'one,', 'final', 'entry'} + + +- describe tfind: + - before: + subject = 'abc' + + f = M.tfind + + - context with bad arguments: + badargs.diagnose(f, 'std.string.tfind(string, string, ?int, ?boolean|:plain)') + + - it creates a list of pattern captures: + target = {1, 3, {'a', 'b', 'c'}} + expect({f(subject, '(.)(.)(.)')}).to_equal(target) + - it creates an empty list where no captures are matched: + target = {nil, nil, {}} + expect({f(subject, '(x)(y)(z)')}).to_equal(target) + - it creates an empty list for a pattern without captures: + target = {1, 1, {}} + expect({f(subject, 'a')}).to_equal(target) + - it starts the search at a specified index into the subject: + target = {8, 10, {'a', 'b', 'c'}} + expect({f('garbage' .. subject, '(.)(.)(.)', 8)}).to_equal(target) + - it is available as a string metamethod: + target = {8, 10, {'a', 'b', 'c'}} + expect({('garbage' .. subject):tfind('(.)(.)(.)', 8)}).to_equal(target) + - it does not perturb the original subject: + original = subject + newstring = f(subject, '...') + expect(subject).to_be(original) + + +- describe trim: + - before: + subject = ' \t\r\n a short string \t\r\n ' + + f = M.trim + + - context with bad arguments: + badargs.diagnose(f, 'std.string.trim(string, ?string)') + + - it removes whitespace from each end of a string: + target = 'a short string' + expect(f(subject)).to_equal(target) + - it supports custom removal patterns: + target = '\r\n a short string \t\r' + expect(f(subject, '[ \t\n]+')).to_equal(target) + - it is available as a string metamethod: + target = '\r\n a short string \t\r' + expect(subject:trim('[ \t\n]+')).to_equal(target) + - it does not perturb the original subject: + original = subject + newstring = f(subject, '%W') + expect(subject).to_be(original) + + +- describe wrap: + - before: + subject = 'This is a collection of Lua libraries for Lua 5.1 ' .. + 'and 5.2. The libraries are copyright by their authors 2000' .. + '-2015 (see the AUTHORS file for details), and released und' .. + 'er the MIT license (the same license as Lua itself). There' .. + ' is no warranty.' + + f = M.wrap + + - context with bad arguments: + badargs.diagnose(f, 'std.string.wrap(string, ?int, ?int, ?int)') + + - it inserts newlines to wrap a string: + target = 'This is a collection of Lua libraries for Lua 5.1 a' .. + 'nd 5.2. The libraries are\ncopyright by their authors 2000' .. + '-2015 (see the AUTHORS file for details), and\nreleased un' .. + 'der the MIT license (the same license as Lua itself). Ther' .. + 'e is no\nwarranty.' + expect(f(subject)).to_be(target) + - it honours a column width parameter: + target = 'This is a collection of Lua libraries for Lua 5.1 a' .. + 'nd 5.2. The libraries\nare copyright by their authors 2000' .. + '-2015 (see the AUTHORS file for\ndetails), and released un' .. + 'der the MIT license (the same license as Lua\nitself). The' .. + 're is no warranty.' + expect(f(subject, 72)).to_be(target) + - it supports indenting by a fixed number of columns: + target = ' This is a collection of Lua libraries for L' .. + 'ua 5.1 and 5.2. The\n libraries are copyright by th' .. + 'eir authors 2000-2015 (see the\n AUTHORS file for d' .. + 'etails), and released under the MIT license\n (the ' .. + 'same license as Lua itself). There is no warranty.' + expect(f(subject, 72, 8)).to_be(target) + - context given a long unwrapped string: + - before: + target = ' This is a collection of Lua libraries for Lua 5' .. + '.1 and 5.2.\n The libraries are copyright by their author' .. + 's 2000-2015 (see\n the AUTHORS file for details), and rel' .. + 'eased under the MIT\n license (the same license as Lua it' .. + 'self). There is no\n warranty.' + - it can indent the first line differently: + expect(f(subject, 64, 2, 4)).to_be(target) + - it is available as a string metamethod: + expect(subject:wrap(64, 2, 4)).to_be(target) + - it does not perturb the original subject: + original = subject + newstring = f(subject, 55, 5) + expect(subject).to_be(original) + - it diagnoses indent greater than line width: + expect(f(subject, 10, 12)).to_raise('less than the line width') + expect(f(subject, 99, 99)).to_raise('less than the line width') + - it diagnoses non-string arguments: + if have_typecheck then + expect(f()).to_raise('string expected') + expect(f {'a table'}).to_raise('string expected') + end diff --git a/Data/BuiltIn/Libraries/lua-stdlib/spec/table_spec.yaml b/Data/BuiltIn/Libraries/lua-stdlib/spec/table_spec.yaml new file mode 100644 index 0000000..d9fed0c --- /dev/null +++ b/Data/BuiltIn/Libraries/lua-stdlib/spec/table_spec.yaml @@ -0,0 +1,589 @@ +# General Lua Libraries for Lua 5.1, 5.2 & 5.3 +# Copyright (C) 2011-2018 stdlib authors + +before: | + base_module = 'table' + this_module = 'std.table' + global_table = '_G' + + extend_base = {'clone', 'clone_select', 'depair', 'empty', + 'enpair', 'insert', 'invert', 'keys', 'maxn', + 'merge', 'merge_select', 'new', + 'pack', 'project', 'remove', 'size', 'sort', + 'unpack', 'values'} + + M = require(this_module) + + +specify std.table: +- context when required: + - context by name: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by=this_module}). + to_equal {} + - it does not touch the core table table: + expect(show_apis {added_to=base_module, by=this_module}). + to_equal {} + - it contains apis from the core table table: + expect(show_apis {from=base_module, not_in=this_module}). + to_contain.a_permutation_of(extend_base) + + - context via the std module: + - it does not touch the global table: + expect(show_apis {added_to=global_table, by='std'}).to_equal {} + - it does not touch the core table table: + expect(show_apis {added_to=base_module, by='std'}).to_equal {} + + +- describe clone: + - before: + subject = {k1={'v1'}, k2={'v2'}, k3={'v3'}} + withmt = setmetatable(M.clone(subject), {'meta!'}) + + f = M.clone + + - context with bad arguments: + badargs.diagnose(f, 'std.table.clone(table, [table], ?boolean|:nometa)') + + - it does not just return the subject: + expect(f(subject)).not_to_be(subject) + - it does copy the subject: + expect(f(subject)).to_equal(subject) + - it only makes a shallow copy of field values: + expect(f(subject).k1).to_be(subject.k1) + - it does not perturb the original subject: + target = {k1=subject.k1, k2=subject.k2, k3=subject.k3} + copy = f(subject) + expect(subject).to_equal(target) + expect(subject).to_be(subject) + + - context with metatables: + - it copies the metatable by default: + expect(getmetatable(f(withmt))).to_be(getmetatable(withmt)) + - it treats non-table arg2 as nometa parameter: + expect(getmetatable(f(withmt, ':nometa'))).to_be(nil) + - it treats table arg2 as a map parameter: + expect(getmetatable(f(withmt, {}))).to_be(getmetatable(withmt)) + - it supports 3 arguments with nometa as arg3: + expect(getmetatable(f(withmt, {}, ':nometa'))).to_be(nil) + + - context when renaming some keys: + - it renames during cloning: + target = {newkey=subject.k1, k2=subject.k2, k3=subject.k3} + expect(f(subject, {k1 = 'newkey'})).to_equal(target) + - it does not perturb the value in the renamed key field: + expect(f(subject, {k1 = 'newkey'}).newkey).to_be(subject.k1) + + +- describe clone_select: + - before: + subject = {k1={'v1'}, k2={'v2'}, k3={'v3'}} + withmt = setmetatable(M.clone(subject), {'meta!'}) + + f = M.clone_select + + - context with bad arguments: + badargs.diagnose(f, 'std.table.clone_select(table, [table], ?boolean|:nometa)') + + - it does not just return the subject: + expect(f(subject)).not_to_be(subject) + - it copies the keys selected: + expect(f(subject, {'k1', 'k2'})).to_equal({k1={'v1'}, k2={'v2'}}) + - it does copy the subject when supplied with a full list of keys: + expect(f(subject, {'k1', 'k2', 'k3'})).to_equal(subject) + - it only makes a shallow copy: + expect(f(subject, {'k1'}).k1).to_be(subject.k1) + - it does not perturb the original subject: + target = {k1=subject.k1, k2=subject.k2, k3=subject.k3} + copy = f(subject, {'k1', 'k2', 'k3'}) + expect(subject).to_equal(target) + expect(subject).to_be(subject) + + - context with metatables: + - it treats non-table arg2 as nometa parameter: + expect(getmetatable(f(withmt, ':nometa'))).to_be(nil) + - it treats table arg2 as a map parameter: + expect(getmetatable(f(withmt, {}))).to_be(getmetatable(withmt)) + expect(getmetatable(f(withmt, {'k1'}))).to_be(getmetatable(withmt)) + - it supports 3 arguments with nometa as arg3: + expect(getmetatable(f(withmt, {}, ':nometa'))).to_be(nil) + expect(getmetatable(f(withmt, {'k1'}, ':nometa'))).to_be(nil) + + +- describe depair: + - before: + t = {'first', 'second', third = 4} + l = M.enpair(t) + + f = M.depair + + - context with bad arguments: + badargs.diagnose(f, 'std.table.depair(list of lists)') + + - it returns a primitive table: + expect(objtype(f(l))).to_be 'table' + - it works with an empty table: + expect(f {}).to_equal {} + - it is the inverse of enpair: + expect(f(l)).to_equal(t) + + +- describe empty: + - before: + f = M.empty + + - context with bad arguments: + badargs.diagnose(f, 'std.table.empty(table)') + + - it returns true for an empty table: + expect(f {}).to_be(true) + expect(f {nil}).to_be(true) + - it returns false for a non-empty table: + expect(f {'stuff'}).to_be(false) + expect(f {{}}).to_be(false) + expect(f {false}).to_be(false) + + +- describe enpair: + - before: + t = {'first', 'second', third = 4} + l = M.enpair(t) + + f = M.enpair + + - context with bad arguments: + badargs.diagnose(f, 'std.table.enpair(table)') + + - it returns a table: + expect(objtype(f(t))).to_be 'table' + - it works for an empty table: + expect(f {}).to_equal {} + - it turns a table into a table of pairs: + expect(f(t)).to_equal {{1, 'first'}, {2, 'second'}, {'third', 4}} + - it is the inverse of depair: + expect(f(t)).to_equal(l) + + +- describe insert: + - before: + f, badarg = init(M, this_module, 'insert') + + - context with bad arguments: + badargs.diagnose(f, 'std.table.insert(table, [int], any)') + + examples { + ['it diagnoses more than 2 arguments with no pos'] = function() + pending '#issue 76' + expect(f({}, false, false)).to_raise(badarg(3)) + end + } + examples { + ['it diagnoses out of bounds pos arguments'] = function() + expect(f({}, 0, 'x')).to_raise 'position 0 out of bounds' + expect(f({}, 2, 'x')).to_raise 'position 2 out of bounds' + expect(f({1}, 5, 'x')).to_raise 'position 5 out of bounds' + end + } + + - it returns the modified table: + t = {} + expect(f(t, 1)).to_be(t) + - it append a new element at the end by default: + expect(f({1, 2}, 'x')).to_equal {1, 2, 'x'} + - it fills holes by default: + expect(f({1, 2, [5]=3}, 'x')).to_equal {1, 2, 'x', [5]=3} + - it respects __len when appending: + t = setmetatable({1, 2, [5]=3}, {__len = function() return 42 end}) + expect(f(t, 'x')).to_equal {1, 2, [5]=3, [43]='x'} + - it moves other elements up if necessary: + expect(f({1, 2}, 1, 'x')).to_equal {'x', 1, 2} + expect(f({1, 2}, 2, 'x')).to_equal {1, 'x', 2} + expect(f({1, 2}, 3, 'x')).to_equal {1, 2, 'x'} + - it inserts a new element according to pos argument: + expect(f({}, 1, 'x')).to_equal {'x'} + + +- describe invert: + - before: + subject = {k1=1, k2=2, k3=3} + + f = M.invert + + - context with bad arguments: + badargs.diagnose(f, 'std.table.invert(table)') + + - it returns a new table: + expect(f(subject)).not_to_be(subject) + - it inverts keys and values in the returned table: + expect(f(subject)).to_equal {'k1', 'k2', 'k3'} + - it is reversible: + expect(f(f(subject))).to_equal(subject) + - it seems to copy a list of 1..n numbers: + subject = {1, 2, 3} + expect(f(subject)).to_copy(subject) + + +- describe keys: + - before: + subject = {k1=1, k2=2, k3=3} + + f = M.keys + + - context with bad arguments: + badargs.diagnose(f, 'std.table.keys(table)') + + - it returns an empty list when subject is empty: + expect(f {}).to_equal {} + - it makes a list of table keys: + cmp = function(a, b) + return a < b + end + expect(M.sort(f(subject), cmp)).to_equal {'k1', 'k2', 'k3'} + - it does not guarantee stable ordering: + subject = {} + -- is this a good test? there is a vanishingly small possibility the + -- returned table will have all 10000 keys in the same order... + for i = 10000, 1, -1 do + table.insert(subject, i) + end + expect(f(subject)).not_to_equal(subject) + + +- describe maxn: + - before: + f = M.maxn + + - context with bad arguments: + badargs.diagnose(f, 'std.table.maxn(table)') + + - it returns the largest numeric key of a table: + expect(f {'a', 'b', 'c'}).to_be(3) + expect(f {1, 2, 5, a=10, 3}).to_be(4) + - it works with an empty table: + expect(f {}).to_be(0) + - it ignores holes: + expect(f {1, 2, [5]=3}).to_be(5) + - it ignores __len metamethod: + t = setmetatable({1, 2, [5]=3}, {__len = function() return 42 end}) + expect(f(t)).to_be(5) + + +- describe merge: + - before: | + -- Additional merge keys which are moderately unusual + t1 = {k1={'v1'}, k2='if', k3={'?'}} + t2 = {['if']=true, [{'?'}]=false, _='underscore', k3=t1.k1} + t1mt = setmetatable(M.clone(t1), {'meta!'}) + target = {} + for k, v in pairs(t1) do + target[k] = v + end + for k, v in pairs(t2) do + target[k] = v + end + + f, badarg = init(M, this_module, 'merge') + + - context with bad arguments: + badargs.diagnose(f, 'std.table.merge(table, table, [table], ?boolean|:nometa)') + + examples { + ['it diagnoses more than 2 arguments with no pos'] = function() + pending '#issue 76' + expect(f({}, {}, ':nometa', false)).to_raise(badarg(4)) + end + } + + - it does not create a whole new table: + expect(f(t1, t2)).to_be(t1) + - it does not change t1 when t2 is empty: + expect(f(t1, {})).to_be(t1) + - it copies t2 when t1 is empty: + expect(f({}, t1)).to_copy(t1) + - it merges keys from t2 into t1: + expect(f(t1, t2)).to_equal(target) + - it gives precedence to values from t2: + original = M.clone(t1) + m = f(t1, t2) -- Merge is destructive, do it once only. + expect(m.k3).to_be(t2.k3) + expect(m.k3).not_to_be(original.k3) + - it only makes a shallow copy of field values: + expect(f({}, t1).k1).to_be(t1.k1) + + - context with metatables: + - it copies the metatable by default: + expect(getmetatable(f({}, t1mt))).to_be(getmetatable(t1mt)) + expect(getmetatable(f({}, t1mt, {'k1'}))).to_be(getmetatable(t1mt)) + - it treats non-table arg3 as nometa parameter: + expect(getmetatable(f({}, t1mt, ':nometa'))).to_be(nil) + - it treats table arg3 as a map parameter: + expect(getmetatable(f({}, t1mt, {}))).to_be(getmetatable(t1mt)) + expect(getmetatable(f({}, t1mt, {'k1'}))).to_be(getmetatable(t1mt)) + - it supports 4 arguments with nometa as arg4: + expect(getmetatable(f({}, t1mt, {}, ':nometa'))).to_be(nil) + expect(getmetatable(f({}, t1mt, {'k1'}, ':nometa'))).to_be(nil) + + - context when renaming some keys: + - it renames during merging: + target = {newkey=t1.k1, k2=t1.k2, k3=t1.k3} + expect(f({}, t1, {k1 = 'newkey'})).to_equal(target) + - it does not perturb the value in the renamed key field: + expect(f({}, t1, {k1 = 'newkey'}).newkey).to_be(t1.k1) + + +- describe merge_select: + - before: | + -- Additional merge keys which are moderately unusual + tablekey = {'?'} + t1 = {k1={'v1'}, k2='if', k3={'?'}} + t1mt = setmetatable(M.clone(t1), {'meta!'}) + t2 = {['if']=true, [tablekey]=false, _='underscore', k3=t1.k1} + t2keys = {'if', tablekey, '_', 'k3'} + target = {} + for k, v in pairs(t1) do + target[k] = v + end + for k, v in pairs(t2) do + target[k] = v + end + + f, badarg = init(M, this_module, 'merge_select') + + - context with bad arguments: + badargs.diagnose(f, 'std.table.merge_select(table, table, [table], ?boolean|:nometa)') + + examples { + ['it diagnoses more than 2 arguments with no pos'] = function() + pending '#issue 76' + expect(f({}, {}, ':nometa', false)).to_raise(badarg(4)) + end + } + + - it does not create a whole new table: + expect(f(t1, t2)).to_be(t1) + - it does not change t1 when t2 is empty: + expect(f(t1, {})).to_be(t1) + - it does not change t1 when key list is empty: + expect(f(t1, t2, {})).to_be(t1) + - it copies the named fields: + expect(f({}, t2, t2keys)).to_equal(t2) + - it makes a shallow copy: + expect(f({}, t1, {'k1'}).k1).to_be(t1.k1) + - it copies exactly named fields of t2 when t1 is empty: + expect(f({}, t1, {'k1', 'k2', 'k3'})).to_copy(t1) + - it merges keys from t2 into t1: + expect(f(t1, t2, t2keys)).to_equal(target) + - it gives precedence to values from t2: + original = M.clone(t1) + m = f(t1, t2, t2keys) -- Merge is destructive, do it once only. + expect(m.k3).to_be(t2.k3) + expect(m.k3).not_to_be(original.k3) + + - context with metatables: + - it copies the metatable by default: + expect(getmetatable(f({}, t1mt))).to_be(getmetatable(t1mt)) + expect(getmetatable(f({}, t1mt, {'k1'}))).to_be(getmetatable(t1mt)) + - it treats non-table arg3 as nometa parameter: + expect(getmetatable(f({}, t1mt, ':nometa'))).to_be(nil) + - it treats table arg3 as a map parameter: + expect(getmetatable(f({}, t1mt, {}))).to_be(getmetatable(t1mt)) + expect(getmetatable(f({}, t1mt, {'k1'}))).to_be(getmetatable(t1mt)) + - it supports 4 arguments with nometa as arg4: + expect(getmetatable(f({}, t1mt, {}, ':nometa'))).to_be(nil) + expect(getmetatable(f({}, t1mt, {'k1'}, ':nometa'))).to_be(nil) + + +- describe new: + - before: + f = M.new + + - context with bad arguments: + badargs.diagnose(f, 'std.table.new(?any, ?table)') + + - context when not setting a default: + - before: default = nil + - it returns a new table when nil is passed: + expect(f(default, nil)).to_equal {} + - it returns any table passed in: + t = {'unique table'} + expect(f(default, t)).to_be(t) + + - context when setting a default: + - before: + default = 'default' + - it returns a new table when nil is passed: + expect(f(default, nil)).to_equal {} + - it returns any table passed in: + t = {'unique table'} + expect(f(default, t)).to_be(t) + + - it returns the stored value for existing keys: + t = f('default') + v = {'unique value'} + t[1] = v + expect(t[1]).to_be(v) + - it returns the constructor default for unset keys: + t = f('default') + expect(t[1]).to_be 'default' + - it returns the actual default object: + default = {'unique object'} + t = f(default) + expect(t[1]).to_be(default) + + +- describe pack: + - before: + unpack = unpack or table.unpack + t = {'one', 'two', 'five', n=3} + f = M.pack + - it creates an empty table with no arguments: + expect(f()).to_equal {n=0} + - it creates a table with arguments as elements: + expect(f('one', 'two', 'five')).to_equal(t) + - it is the inverse operation to unpack: + expect(f(unpack(t))).to_equal(t) + - it saves the tuple length in field n: + expect(f(1, 2, 5).n).to_be(3) + expect(f('', false, nil).n).to_be(3) + expect(f(nil, nil, nil).n).to_be(3) + + +- describe project: + - before: + l = { + {first = false, second = true, third = true}, + {first = 1, second = 2, third = 3}, + {first = '1st', second = '2nd', third = '3rd'}, + } + + f = M.project + + - context with bad arguments: + badargs.diagnose(f, 'std.table.project(any, list of tables)') + + - it returns a table: + expect(objtype(f('third', l))).to_be 'table' + - it works with an empty table: + expect(f('third', {})).to_equal {} + - it projects a table of fields from a table of tables: + expect(f('third', l)).to_equal {true, 3, '3rd'} + - it projects fields with a falsey value correctly: + expect(f('first', l)).to_equal {false, 1, '1st'} + + +- describe remove: + - before: + f = M.remove + + - context with bad arguments: + badargs.diagnose(f, 'std.table.remove(table, ?int)') + + examples { + ['it diagnoses out of bounds pos arguments'] = function() + expect(f({1}, 0)).to_raise 'position 0 out of bounds' + expect(f({1}, 3)).to_raise 'position 3 out of bounds' + expect(f({1}, 5)).to_raise 'position 5 out of bounds' + end + } + + - it returns the removed element: + t = {'one', 'two', 'five'} + expect(f({'one', 2, 5}, 1)).to_be 'one' + - it removes an element from the end by default: + expect(f {1, 2, 'five'}).to_be 'five' + - it ignores holes: + t = {'second', 'first', [5]='invisible'} + expect(f(t)).to_be 'first' + expect(f(t)).to_be 'second' + - it respects __len when defaulting pos: + t = setmetatable({1, 2, [43]='invisible'}, {__len = function() return 42 end}) + expect(f(t)).to_be(nil) + expect(f(t)).to_be(nil) + expect(t).to_equal {1, 2, [43]='invisible'} + - it moves other elements down if necessary: + t = {1, 2, 5, 'third', 'first', 'second', 42} + expect(f(t, 5)).to_be 'first' + expect(t).to_equal {1, 2, 5, 'third', 'second', 42} + expect(f(t, 5)).to_be 'second' + expect(t).to_equal {1, 2, 5, 'third', 42} + expect(f(t, 4)).to_be 'third' + expect(t).to_equal {1, 2, 5, 42} + + +- describe size: + - before: | + -- - 1 - ------- 2 ------- -- 3 -- + subject = {'one', {{'two'}, 'three'}, four=5} + + f = M.size + + - context with bad arguments: + badargs.diagnose(f, 'std.table.size(table)') + + - it counts the number of keys in a table: + expect(f(subject)).to_be(3) + - it counts no keys in an empty table: + expect(f {}).to_be(0) + + +- describe sort: + - before: + subject = {5, 2, 4, 1, 0, 3} + target = {0, 1, 2, 3, 4, 5} + cmp = function(a, b) return a < b end + + f = M.sort + + - context with bad arguments: + badargs.diagnose(f, 'std.table.sort(table, ?function)') + + - it sorts elements in place: + f(subject, cmp) + expect(subject).to_equal(target) + - it returns the sorted table: + expect(f(subject, cmp)).to_equal(target) + + +- describe unpack: + - before: + t = {'one', 'two', 'five'} + f = M.unpack + - it returns nil for an empty table: + expect(f {}).to_be(nil) + - it returns numeric indexed table elements: + expect({f(t)}).to_equal(t) + - it respects __len metamethod: + function two(t) + return setmetatable(t, {__len=function() return 2 end}) + end + expect(pack(f(two {})).n).to_be(2) + expect(pack(f(two(t))).n).to_be(2) + - it returns holes in numeric indices as nil: + expect({f {nil, 2}}).to_equal {[2] = 2} + expect({f {nil, nil, 3}}).to_equal {[3] = 3} + expect({f {1, nil, nil, 4}}).to_equal {1, [4] = 4} + - it is the inverse operation to pack: + expect({f(M.pack('one', 'two', 'five'))}).to_equal(t) + + +- describe values: + - before: + subject = {k1={1}, k2={2}, k3={3}} + + f = M.values + + - context with bad arguments: + badargs.diagnose(f, 'std.table.values(table)') + + - it returns an empty list when subject is empty: + expect(f {}).to_equal {} + - it makes a list of table values: + cmp = function(a, b) return a[1] < b[1] end + expect(M.sort(f(subject), cmp)).to_equal {{1}, {2}, {3}} + - it does guarantee stable ordering: + subject = {} + -- is this a good test? or just requiring an implementation quirk? + for i = 10000, 1, -1 do + table.insert(subject, i) + end + expect(f(subject)).to_equal(subject) |