Bruker:Jeblad/Module:BDD
- Note that the module is not ready for production, it is still under active development!
The purpose of this module is to support behavior-driven development (BDD), a software development process that emerged from test-driven development (TDD), in on-going development of Lua-based modules. It makes an assumption of a test module on a separate page, possibly a subpage, and presentation on another page like the talk page or generated through an API call.
The module mimics some ideas from other BDD libs, like RSpec [1], Jasmine [2], and Busted [3]. There are no clear standard on how to do this, so there are some variation and adaption.
The module is not built for great speed, it uses closures to build a number of small objects, that is methods returns a value that holds a closure. This creates a fairly efficient implementation for simple access, but it is not very efficient for caching larger structures. Some larger structures are put in tables and built once. This is done for Expect
where each new instance would otherwise create a lot of methods.
The module avoids as much caching as possible, as this can poison the tests.
Usage
If you have a module like «Module:HelloWorld», the ubiquitous and quite pesky example, coded as something like
local p = {}
function p.helloWorld()
return "Hi there!"
end
return p
Then on a test page you would test this like like the following
require 'Module:BDD'()
local p = require 'Module:HelloWorld'()
describe('Hello world', function()
context('On all pages', function()
it('says hello', function()
expect('p.helloWorld()', p.helloWorld()):toBe("Hi there!");
end);
end);
end);
return result('Module:HelloWorld')
In the first line the test framework is not only loaded, but it is also installed in the global name space. The trailing parenthesis is a call on the loaded table, and it will install additional global functions. It is not necessary to keep a pointer to the test framework, the functions keep their own pointers.
In the second line the module under test is loaded. Usually it is necessary to keep a pointer to the returned value.
The describe
(alt Given
) function adds a title and evaluates the function from a xpcall
(not done yet). The same happens with context
(alt When
) and it
(alt Then
). These three are really the same, it is only our interpretation that changes.
The expect
function adds a comment and the statement that creates the actual value. It then builds a closure and returns a structure in a flow style. This makes it possible to manipulate the instance, before we finally evaluates it and stores the result for later visualization. In this case we simply tests it to see if it is equal.
The final results are returned by a call to result
. Like before this can add a title.
The tests can be prepared for for localization into several languages, for example English and Norwegian Bokmål
require 'Module:BDD'()
local p = require 'Module:HelloWorld'()
describe({en='Hello world', nb='Hallo verden'}, function()
context({en='on all pages', nb='på alle sider'}, function()
it({en='says hello', nb='sier hallo'}, function()
expect('p.helloWorld()', p.helloWorld()):toBe("Hi there!");
end);
end);
end);
return result({ en='Module:HelloWorld', nb='Module:HelloWorld'})
In this case the languages will be chosen according to the site content language, and if that isn't defined it will use the languages from the fallback chain. If the first argument is a string, then that string will always be used. The language can be changed if the tests are run through the api, which makes it easy to check tests if they are prepared for the requested language.
Usually it is good practice to both use a common language to make the tests readable and reusable, and use a local language to make them readable for the local community at each project.
Further work
At present formatting does not work properly. It is expected to be fixed. ;)
Setup/teardown
There will be a solution whereby setup and teardown is chained. That means that the tests are somewhat isolated from each other at each test level, that is each time we define a describe, context, or it, the environment will be recreated. State can still leak between expectations though.
It is assumed that there will be no global setup and teardown as there is no resource allocation available from pages in Scribunto.
Xpcall
All calls to provided functions should be protected in xpcalls so a single exception does not make the whole test set to fail. A failed function should be marked as such in the report.
Actual function is xpcall, and it will always add a stack trace on our own stack. The entry might be without a message.
This is partially implemented, proper formatting remains unsolved.
Spy on public calls
Note that spying on public calls made before the module is available to the testing regime will not be possible.
- Carp
- Adds a message to stack without exiting the test, printing the callers name and its arguments.
- Cluck
- Like Carp, but also prints a stack trace starting one level up.
- Croak/Confess
- Like Carp, but also stops the running test (the user provided anonymous function). Because it throws an exception it will always trigger a stack trace.
Call would be like [carp|cluck|croak|confess](message, table, method)
.
Set up of spies are implemented, but they should be removed during teardown. Such cleanup must check if the struct is still available. This may happen if structs are created inside the test environment.
Coverage for public call
It should be possible to register spies for profiling coverage on members of a module. A function should loop over all members and register spies for all of them. Coverage can then be calculated for the public members. This will only give coverage of the entry points, but it seems sufficient to get a coarse number for coverage.
Stub public methods
It should be possible to stub public methods from the test page. This will not include private methods. (Is this something that should be part of a BDD module?)
Module return
At present an explicit return must be done. It seems to be possible to do an implicit return. This makes a slightly less error prone setup procedure.
Several options exist in literature, but it might be some limitations to what is actually allowed in Scribunto. It seems like all options are blocked.
Test Anything Protocol
At some point the Test Anything Protocol (TAP) should be supported. The simplest solution is to use yet another page for that response, but it is also possible to test on the page name. This can be different if we transclude the talk page into some other page, or even constructs the page on the fly through the api. If the transcluded page name is "API" then we switch to a TAP response. This makes it possible to support several formats from a single page.
Support libs
- Module:BDD/utils – Utility functions for testing tables and table contents. The functions are roughly equal to the same methods in Yonabas Moses library. [4]
- Module:BDD/view – View to show the result of the tests.
- Module:BDD/localize – Access methods to the localization libraries and the localized strings.
- Special:Prefixindex/Module:BDD/localize/ – The localized strings for each language.
See also
-- module for Behavior Driven Development testing framework
-- © John Erling Blad, Creative Commons by Attribution 3.0
-- @var The table holding this modules exported members
local bdd = {}
-- statefull function for constructing unique number sequences
-- @return number from an unique sequence
local idrunner = (function()
local counter = 0
return function()
counter = 1+counter
return tostring( counter )
end
end)()
-- escape a string to make it a valid name for classes and add a prefix
-- @param str string used as the source for the class name
-- @param pref string used as the prefix
-- @return string escaped and prefixed
local function escClass( str, prefix )
assert(str, "invalid call: String not defined")
function helper( char )
return string.format ( "$%02X", string.byte( char ) )
end
str = string.gsub( str, "[%s]", "_" )
str = string.gsub( str, "([^%w%-%_])", helper )
if prefix then
prefix = string.gsub( str, "[%s]", "_" )
prefix = string.gsub( str, "([^%w%-%_])", helper )
end
return (prefix or 'bdd') .. '-' .. str
end
-- raw count of all the items in the provided table
local function count( t )
local i = 0
for k,v in pairs( t ) do i = i + 1 end
return i
end
-- size based on the raw count
local function size(...)
local args = {...}
local arg1 = args[1]
if arg1 == nil then
return 0
elseif type( arg1 ) == 'table' then
return count( args[1] )
else
return count( args )
end
end
-- deep equal of two objects
local function deepEqual( objA, objB, useMt )
local typeObjA = type( objA )
local typeObjB = type( objB )
if typeObjA ~= typeObjB then
return false end
if typeObjA ~= 'table' then
return objA == objB
end
local mtA = getmetatable( objA )
local mtB = getmetatable( objB )
if useMt then
if (mtA or mtB) and (mtA.__eq or mtB.__eq) then
return mtA.__eq(objA, objB) or mtB.__eq(objB, objA) or (objA==objB)
end
end
if size( objA ) ~= size( objB ) then
return false
end
for i,v1 in pairs( objA ) do
local v2 = objB[i]
if v2 == nil or not deepEqual( v1, v2, useMt ) then
return false
end
end
for i,v1 in pairs(objB) do
local v2 = objA[i]
if v2 == nil then
return false
end
end
return true
end
function contains(t, arg)
local cmp = (type(arg)=='function') and arg or deepEqual
for k,v in pairs(t) do
if cmp(v, arg) then
return k
end
end
return false
end
local function View( tbl, stats )
local self = {}
local truncateLength = 30
-- @var _params to be used unless there are local overrides
local messages = {}
messages['result'] = 'Result $1'
messages['pending'] = 'Pending $1'
messages['describe'] = 'Describe $1'
messages['context'] = 'Context $1'
messages['it'] = 'It $1'
messages['xdescribe'] = 'Mute describe $1'
messages['xcontext'] = 'Mute context $1'
messages['xit'] = 'Mute it $1'
messages['exception'] = 'Catch $1'
messages['stack'] = 'Stack'
messages['carp'] = 'Carp'
messages['cluck'] = 'Cluck'
messages['croak'] = 'Croak'
messages['spy'] = 'Spy $1'
messages['spy-args'] = 'Spy $1'
messages['spy-no-formatter'] = 'Spy $1'
messages['actual'] = 'actual ($1)'
--messages['actual-noargs'] = 'actual is empty'
messages['actual-noargs'] = ''
messages['anticipated'] = 'and anticipate ($1),'
--messages['anticipated-noargs'] = 'and anticipate is empty'
messages['anticipated-noargs'] = ''
messages['expect'] = 'Expect $1'
messages['expect-args'] = 'Expect $1'
messages['expect-no-formatter'] = 'Expect $1'
messages['as-upper'] = 'as uppercase'
messages['as-lower'] = 'as lowercase'
messages['as-upper-first'] = 'as uppercase first'
messages['as-lower-first'] = 'as lowercase first'
messages['as-identity'] = 'as identity "$1"'
messages['when-invert'] = 'and then invert ($1)'
messages['when-identity'] = 'and then as identity "$1"'
messages['if-boolean'] = ', it is boolean type'
messages['if-string'] = ', it is string type'
messages['if-number'] = ', it is number type'
messages['if-table'] = ', it is table type'
messages['if-nil'] = 'it is nil'
messages['if-fail'] = ', force fail' --change
messages['if-equal'] = 'compare with equal ($1)'
messages['if-deep-equal'] = 'compare with deep equal ($1)'
messages['if-lesser-than'] = 'if lesser than, then'
messages['if-greater-than'] = 'if greather than, then'
messages['if-lesser-or-equal'] = 'if lesser or equal, then'
messages['if-greater-or-equal'] = 'if greater or equal, then'
messages['if-false'] = 'compare with false'
messages['if-true'] = 'compare with true'
messages['if-falsy'] = 'compare with falsy'
messages['if-truthy'] = 'compare with truthy'
messages['if-first-identity'] = 'first identity to be "$1"'
messages['if-second-identity'] = 'second identity to be "$1"'
messages['if-match'] = 'if it match ($1)'
messages['if-match-noargs'] = 'if it match nothing'
messages['if-ustring-match'] = 'if it match ($1)'
messages['if-ustring-match-noargs'] = 'if it match nothing'
messages['if-contains'] = 'if it contains value ($1)'
messages['if-close-to'] = 'if value is close ($1)'
messages['to-be-boolean'] = 'to be a boolean'
messages['to-be-string'] = 'to be a string'
messages['to-be-number'] = 'to be a number'
messages['to-be-table'] = 'to be a table'
messages['to-be-nil'] = 'to be a nil'
messages['to-be'] = 'to become "$1"'
messages['to-be-failing'] = 'to be failing'
--messages['to-be-equal'] = 'to be equal ($1)'
messages['to-be-deep-equal'] = 'to be deep equal ($1)'
messages['to-be-lesser-than'] = 'to be lesser than "$1"'
messages['to-be-greater-than'] = 'to be greater than'
messages['to-be-lesser-or-equal'] = 'to be lesser or equal'
messages['to-be-greater-or-equal'] = 'to be greater or equal'
messages['to-be-falsy'] = 'to be falsy'
messages['to-be-truthy'] = 'to be truthy'
messages['to-be-false'] = 'to be false'
messages['to-be-true'] = 'to be true'
messages['to-be-first-identity'] = 'first identity to be "$1"'
messages['to-be-second-identity'] = 'second identity to be "$1"'
messages['to-be-match'] = 'to match ($1)'
messages['to-be-match-noargs'] = 'to match nothing'
messages['to-be-ustring-match'] = 'to match ($1)'
messages['to-be-ustring-match-noargs'] = 'to match nothing'
messages['to-be-contains'] = 'to contain value ($1)'
messages['to-be-close-to'] = 'to be close to value ($1)'
messages['joiner'] = ', '
messages['no-formatter'] = "[no-formatter:$1]"
messages['skipped'] = "[skipped]"
messages['category-all-tests'] = "All tests"
messages['category-failed-tests'] = "Failed tests"
messages['category-good-tests'] = "Good tests"
local function g( key, ...)
local msg = mw.message.new( 'bdd-' .. key )
if msg:isBlank() then
assert(messages[key],
"invalid message: " .. key .. " is not a defined string" )
msg = mw.message.newRawMessage( messages[key] )
end
return msg
end
local formatters = {}
local function _xdefinition( ... )
local t = {...}
local dt = mw.html.create( 'dt' )
local dl = mw.html.create( 'dl' )
:node( dt )
for i,v in ipairs({...}) do
if i == 1 then
dt:addClass( escClass( v ) )
elseif i == 2 then
dt:wikitext(
g( t[1] )
:params( v )
:plain()
)
end
end
return dl
end
local function _definition( ... )
local t = {...}
local id = idrunner()
local dl = mw.html.create( 'dl' )
local dt = mw.html.create( 'dt' )
dt:addClass( 'mw-customtoggle-' .. id )
local dd = mw.html.create( 'dd' )
dd:addClass( 'mw-collapsible' )
dd:attr( 'id', 'mw-customcollapsible-' .. id )
for i,v in ipairs(t) do
if i == 1 then
dt:addClass( escClass( v ) )
elseif i == 2 then
dt:wikitext(
g( t[1] )
:params( v )
:plain()
)
--elseif i == 3 then
--local msg = g( t[1] )
--dt:wikitext(msg:params( v ):plain() )
--dt:wikitext(v[1])
elseif v and v[1] then
if type(formatters[v[1]]) == 'function' then
local html = formatters[v[1]]( unpack( v ) )
dd:node(
html
)
else
dd:wikitext(
g( 'no-formatter' )
:params(v[1] or 'none')
:plain()
)
end
else
dd:wikitext(
g( 'skipped' )
:plain()
)
end
end
dl:node( dt )
dl:node( dd )
return dl
end
local function _expect( ... )
local t = {...}
local ul = mw.html.create( 'ul' )
for i,v in ipairs(t) do
if i == 1 then
ul:addClass( escClass( v ) )
local last = t[#t]
local final = last[#last]
ul:addClass(
final and 'bdd-expect-good' or 'bdd-expect-fail'
)
elseif i == 2 then
-- delay this
elseif i == 3 then
local li = mw.html.create( 'li' )
if v then
if type(formatters[v[1]]) == 'function' then
local html = formatters[v[1]]( unpack( v ) )
if html then
li:wikitext(
g( 'expect-args' )
:rawParams( mw.text.nowiki( t[2] ) )
:rawParams( html:done() )
:plain()
)
else
li:wikitext(
g( 'expect' )
:rawParams( mw.text.nowiki( t[2] ) )
:plain()
)
end
else
li:wikitext(
g( 'expect-no-formatter' )
:rawParams( mw.text.nowiki( t[2] ) )
:plain()
)
end
else
li:wikitext(
g( 'expect' )
:rawParams( mw.text.nowiki( t[2] ) )
:plain()
)
end
ul:node(li)
elseif i == 4 then
-- skip this for now, it is the statistics
elseif v and v[1] then
local li = mw.html.create( 'li' )
if type(formatters[v[1]]) == 'function' then
local html = formatters[v[1]]( unpack( v ) )
li:node( html )
else
li:wikitext(
g( 'no-formatter' )
:params(v[1] or 'none')
:plain()
)
end
ul:node(li)
else
local li = mw.html.create( 'li' )
dd:wikitext(
g( 'skipped' )
:plain()
)
ul:node(li)
end
end
return ul
end
local function dumpBox( obj )
local dump = mw.dumpObject( obj )
local short = mw.html.create( 'div' )
short
:addClass( 'bdd-popup-handle' )
:css( 'display', 'inline-block' )
:wikitext(
mw.text.nowiki(
mw.text.trim(
mw.text.truncate( dump:gsub('\n', ' '), truncateLength, nil, true )
)
)
)
local box = mw.html.create( 'div' )
box
:addClass( 'bdd-popup-box' )
:css('display', 'none')
local full = mw.html.create( 'pre' )
full
:wikitext(
mw.text.nowiki(
mw.text.trim(
dump
)
)
)
box:node(full)
short:node(box)
return short
end
local function _partial( ... )
local t = {...}
local li = mw.html.create( 'li' )
local html
if #t == 1 then
-- empty params
elseif #t == 2 then
-- single params
html = dumpBox( t[2] )
else
-- several params
end
if html then
li:wikitext(
g( t[1] )
:rawParams( html )
:plain()
)
else
li:wikitext(
g( t[1]..'-noargs' )
:plain()
)
end
return li
end
local function _params( ... )
local t = {...}
local html
if #t == 1 then
-- empty params
elseif #t == 2 then
-- single params
html = dumpBox( t[2] )
else
-- several params
end
return html
end
local function _spy( ... )
local t = {...}
local ul = mw.html.create( 'ul' )
for i,v in ipairs(t) do
if i == 1 then
ul:addClass( escClass( v ) )
local last = t[#t]
local final = last[#last]
ul:addClass(
final and 'bdd-spy-good' or 'bdd-spy-fail'
)
elseif i == 2 then
-- delay this
elseif i == 3 then
local li = mw.html.create( 'li' )
if v then
if type(formatters[v[1]]) == 'function' then
local html = formatters[v[1]]( unpack( v ) )
if html then
li:wikitext(
g( 'spy-args' )
:rawParams( mw.text.nowiki( t[2] ) )
:rawParams( html:done() )
:plain()
)
else
li:wikitext(
g( 'spy' )
:rawParams( mw.text.nowiki( t[2] ) )
:plain()
)
end
else
li:wikitext(
g( 'spy-no-formatter' )
:rawParams( mw.text.nowiki( t[2] ) )
:plain()
)
end
else
li:wikitext(
g( 'spy' )
:rawParams( mw.text.nowiki( t[2] ) )
:plain()
)
end
ul:node(li)
elseif i == 4 then
-- skip this for now, it is the statistics
elseif v and v[1] then
local li = mw.html.create( 'li' )
if type(formatters[v[1]]) == 'function' then
local html = formatters[v[1]]( unpack( v ) )
li:node( html )
else
li:wikitext(
g( 'no-formatter' )
:params(v[1] or 'none')
:plain()
)
end
ul:node(li)
else
local li = mw.html.create( 'li' )
dd:wikitext(
g( 'skipped' )
:plain()
)
ul:node(li)
end
end
return ul
end
local function _nop( ... )
local placeholder = mw.html.create( 'div' )
:css( 'display', 'none' )
return placeholder
end
formatters['result'] = _definition
formatters['describe'] = _definition
formatters['context'] = _definition
formatters['it'] = _definition
formatters['xdescribe'] = _xdefinition
formatters['xcontext'] = _xdefinition
formatters['xit'] = _xdefinition
formatters['expect'] = _expect
formatters['results'] = _results
formatters['params'] = _params
formatters['carp'] = _spy
formatters['cluck'] = _spy
formatters['croak'] = _spy
formatters['actual'] = _partial
formatters['anticipated'] = _partial
formatters['as-upper'] = _partial
formatters['as-lower'] = _partial
formatters['as-upper-first'] = _partial
formatters['as-lower-first'] = _partial
formatters['when-invert'] = _partial
formatters['if-boolean'] = _partial
formatters['if-string'] = _partial
formatters['if-number'] = _partial
formatters['if-table'] = _partial
formatters['if-nil'] = _partial
formatters['if-fail'] = _partial
formatters['if-equal'] = _partial
formatters['if-deep-equal'] = _partial
formatters['if-lesser-than'] = _partial
formatters['if-greater-than'] = _partial
formatters['if-lesser-or-equal'] = _partial
formatters['if-greater-or-equal'] = _partial
formatters['if-false'] = _partial
formatters['if-true'] = _partial
formatters['if-falsy'] = _partial
formatters['if-truthy'] = _partial
formatters['if-match'] = _partial
formatters['if-ustring-match'] = _partial
formatters['if-contains'] = _partial
formatters['if-close-to'] = _partial
formatters['to-be'] = _partial
formatters['to-be-failing'] = _partial
formatters['to-be-deep-equal'] = _partial
formatters['to-be-lesser-than'] = _partial
formatters['to-be-greater-than'] = _partial
formatters['to-be-lesser-or-equal'] = _partial
formatters['to-be-greater-or-equal'] = _partial
formatters['to-be-falsy'] = _partial
formatters['to-be-truthy'] = _partial
formatters['to-be-nil'] = _partial
formatters['to-be-boolean'] = _partial
formatters['to-be-string'] = _partial
formatters['to-be-number'] = _partial
formatters['to-be-table'] = _partial
formatters['to-be-false'] = _partial
formatters['to-be-true'] = _partial
formatters['to-be-match'] = _partial
formatters['to-be-ustring-match'] = _partial
formatters['to-be-contains'] = _partial
formatters['to-be-close-to'] = _partial
function self.format()
local div = mw.html.create( 'div' )
if tbl[1] then
div:node( formatters[tbl[1]]( unpack( tbl ) ) )
end
local rep = (stats and stats.numfailed > 0) and '[[Category:Failed bdd test sets]]' or ''
return div:allDone() -- .. ' ' .. rep
end
return self
end
local function buildName( str )
return str:gsub("([A-Z])", function(str) return '-'..string.lower(str) end )
end
-- constructor for a closure to make the functions use the same stack
local function BDD()
-- @var This is the stack where we store our temporary formatters
local stack = { "result", '' }
-- @var This is the accumulators where we store our statistics
-- local statistics = { "statistics", { "accumulator" , 0, 0 } }
-- @var accumulator for failed tests
local numFailed = 0
-- @var base for expectations
local base = {}
do
local preprocess = {}
preprocess['asUpper'] = function ( str ) return str:upper() end
preprocess['asLower'] = function ( str ) return str:lower() end
preprocess['asUpperFirst'] = function ( str ) return str:sub(1,1):upper()..str:sub(2) end
preprocess['asLowerFirst'] = function ( str ) return str:sub(1,1):lower()..str:sub(2) end
preprocess['asIdentity'] = function ( str ) return str end
for k,v in pairs( preprocess ) do
base[k] = function( self )
self.preprocess[1+#self.preprocess] = { buildName(k), v }
return self
end
end
base.upper = base.asUpper
base.asUC = base.asUpper
base.uc = base.asUpper
base.lower = base.asLower
base.asLC = base.asLower
base.lc = base.asLower
base.upperfirst = base.asUpperFirst
base.asUCFirst = base.asUpperFirst
base.asUcFirst = base.asUpperFirst
base.asUCfirst = base.asUpperFirst
base.ucfirst = base.asUpperFirst
base.lowerfirst = base.asLowerFirst
base.asLCFirst = base.asLowerFirst
base.asLcFirst = base.asLowerFirst
base.asLCfirst = base.asLowerFirst
base.lcfirst = base.asLowerFirst
local postprocess = {}
postprocess['whenInvert'] = function ( bool ) return not bool end
postprocess['whenIdentity'] = function ( bool ) return bool end
for k,v in pairs( postprocess ) do
base[k] = function( self )
self.postprocess[1+#self.postprocess] = { buildName(k), v }
return self
end
end
base.invert = base.whenInvert
base.inverted = base.whenInvert
base.whenInv = base.whenInvert
base.inv = base.whenInvert
end
-- constructor for expectations
local function Expect( str, actual )
local self = {}
setmetatable(self, {__index = base})
-- @var list of processes to run before matching
self.preprocess = {}
-- @var similarity process
self.similarity = {}
-- @var list of processes to run after matching
self.postprocess = {}
local function exec( ... )
local values = {}
-- this is the actual argument
values[1+#values] = { 'actual', actual }
--this is a chain of preprocesses
for _,v in ipairs( self.preprocess ) do
values[1+#values] = { v[1], v[2]( values[#values][2] ) }
end
local keep = #values
-- this is the anticipated result
values[1+#values] = { 'anticipated', ... }
-- this is the actual similarity test
values[1+#values] = { self.similarity[1], self.similarity[2]( values[keep][2], ... ) }
-- this is a chain of postprocesses
for _,v in ipairs( self.postprocess ) do
values[1+#values] = { v[1], v[2]( values[#values][2] ) }
end
return values
end
do
local similarity = {}
similarity['ifBoolean'] = function ( a ) return type( a ) == 'boolean' end
similarity['ifString'] = function ( a ) return type( a ) == 'string' end
similarity['ifNumber'] = function ( a ) return type( a ) == 'number' end
similarity['ifTable'] = function ( a ) return type( a ) == 'table' end
similarity['ifNil'] = function ( a ) return type( a ) == 'nil' end
similarity['ifFail'] = function ( a, b ) return false end
similarity['ifEqual'] = function ( a, b ) return a == b end
similarity['ifDeepEqual'] = function ( a, b ) return deepEqual( a, b) end
similarity['ifLesserThan'] = function ( a, b ) return a < b end
similarity['ifGreaterThan'] = function ( a, b ) return a > b end
similarity['ifLesserOrEqual'] = function ( a, b ) return a <= b end
similarity['ifGreaterOrEqual'] = function ( a, b ) return a >= b end
similarity['ifFalse'] = function ( a ) return type(a) == 'boolean' and a == false end
similarity['ifTrue'] = function ( a ) return type(a) == 'boolean' and a == true end
similarity['ifFalsy'] = function ( a ) return not a end
similarity['ifTruthy'] = function ( a ) return not not a end
similarity['ifFirstIdentity'] = function ( a, b ) return a end
similarity['ifSecondIdentity'] = function ( a, b ) return b end
similarity['ifMatch'] = function ( a, b ) local res = string.match(a, b); return res or false end
similarity['ifUstringMatch'] = function ( a, b ) local res = mw.ustring.match(a, b); return res or false end
similarity['ifContains'] = function ( a, b ) return not not contains( a, b ) end
similarity['ifCloseTo'] = function ( a, b, c ) return b-c <= a and a <= b+c end
for k,v in pairs( similarity ) do
self[k] = function()
self.similarity = { buildName(k), v }
return self
end
end
self.equal = self.ifEqual
self.deepequal = self.ifDeepEqual
self.ifLesser = self.ifLesserThan
self.ifLT = self.ifLesserThan
self.lesser = self.ifLesserThan
self.ifGreater = self.ifGreaterThan
self.ifGT = self.ifGreaterThan
self.greater = self.ifGreaterThan
self.ifLesserEqual = self.ifLesserOrEqual
self.ifLE = self.ifLesserOrEqual
self.lesserequal = self.ifLesserOrEqual
self.ifGreaterEqual = self.ifGreaterOrEqual
self.ifGE = self.ifGreaterOrEqual
self.greaterequal = self.ifGreaterOrEqual
self.False = self.ifFalse
self.True = self.ifTrue
self.Nil = self.ifNil
self.falsy = self.ifFalsy
self.truthy = self.ifTruthy
self.ifContain = self.ifContains
self.contains = self.ifContains
self.contain = self.ifContains
self.closeTo = self.ifCloseTo
-- this is default
self.similarity = { buildName('ifEqual'), similarity['ifEqual'] }
local matchers = {}
matchers['toBe'] = false
matchers['toBeBoolean'] = similarity['ifBoolean']
matchers['toBeString'] = similarity['ifString']
matchers['toBeNumber'] = similarity['ifNumber']
matchers['toBeTable'] = similarity['ifTable']
matchers['toBeNil'] = similarity['ifNil']
matchers['toBeDeepEqual'] = similarity['ifDeepEqual']
matchers['toBeFailing'] = similarity['ifFail']
matchers['toBeLesserThan'] = similarity['ifLesserThan']
matchers['toBeGreaterThan'] = similarity['ifGreaterThan']
matchers['toBeLesserOrEqual'] = similarity['ifLesserOrEqual']
matchers['toBeGreaterOrEqual'] = similarity['ifGreaterOrEqual']
matchers['toBeFalsy'] = similarity['ifFalsy']
matchers['toBeTruthy'] = similarity['ifTruthy']
matchers['toBeFalse'] = similarity['ifFalse']
matchers['toBeTrue'] = similarity['ifTrue']
matchers['toBeFirstIdentity'] = similarity['ifFirstIdentity']
matchers['toBeSecondIdentity'] = similarity['ifSecondIdentity']
matchers['toBeMatch'] = similarity['ifMatch']
matchers['toBeUstringMatch'] = similarity['ifUstringMatch']
matchers['toBeContains'] = similarity['ifContains']
matchers['toBeCloseTo'] = similarity['ifCloseTo']
for k,v in pairs( matchers ) do
self[k] = function( ... )
if v then
self.similarity = { buildName(k), v }
end
local res = exec( ... )
-- @todo must be wrapped up and made html safe
-- str comes from the constructor
local rep = { "expect", str, { "params", ... }, {}, unpack( res ) }
if not res then
numFailed = 1+numFailed
--rep[1+#rep] = debug.traceback()
end
stack[1+#stack] = rep
return self
end
end
self.be = self.toBe
self.beFalsy = self.toBeFalsy
self.beTruthy = self.toBeTruthy
self.beNil = self.toBeNil
self.beMatch = self.toBeMatch
self.toMatch = self.toBeMatch
self.beEqual = self.toBe
self.toEqual = self.toBe
self.beDeepEqual = self.toBeDeepEqual
self.toDeepEqual = self.toBeDeepEqual
self.toContains = self.toBeContains
self.toContain = self.toBeContains
end
-- match for "throw"
-- @return table
function self.toThrow()
return self
end
-- match for "have been called"
-- @return table
function self.toHaveBeenCalled()
return self
end
-- match for "have been called with"
-- @return table
function self.toHaveBeenCalledWith()
return self
end
-- this is our instance
return self
end
-- @var This is the module structure returned from the test set
local wrap = {
-- the test runner
tests = function( frame )
--return mw.dumpObject( stack )
return View( stack, {numfailed=numFailed} ).format()
end,
categories = function(frame)
local cats = {
'[[Category:category-all-tests]]',
--'[[Category:'..g('category-all-tests')..']]',
}
return table.concat(cats, '\n')
end
}
local function stackTrace( msg )
local tbl = {}
tbl[1+#tbl] = "stack"
tbl[1+#tbl] = msg
-- @todo must be made html safe
tbl[1+#tbl] = mw.text.split( debug.traceback(), "\n\t", true )
stack[1+#stack] = tbl
end
local function exceptionTrace( msg )
local tbl = {}
tbl[1+#tbl] = "exception"
tbl[1+#tbl] = msg
-- @todo must be made html safe
tbl[1+#tbl] = mw.text.split( debug.traceback(), "\n\t", true )
stack[1+#stack] = tbl
end
-- this builds a frame and later wraps it up
local function frame( name )
-- build and return an appropriate function
return function( str, func )
-- statistics[1+#statistics] = { "accumulator", 0, 0 }
local depth = 1+#stack
stack[1+#stack] = name
stack[1+#stack] = str
xpcall( func, exceptionTrace )
local tbl = {}
for i=depth,#stack,1 do
tbl[1+#tbl] = stack[i]
stack[i] = nil
end
-- local last = statistics[#statistics]
--table.insert(tbl, 3, last)
--local prev = statistics[#statistics-1]
--prev[2] = prev[2]+last[2]
--prev[3] = prev[3]+last[3]
-- table.remove(statistics)
stack[1+#stack] = tbl
end
end
-- this builds a xframe and later wraps it up
local function xframe( name )
-- build and return an appropriate function
return function( str, func )
stack[1+#stack] = { name, str }
end
end
-- this builds spies by monkey patching
-- note that it seems impossible to store at a funcs location
-- @todo still needs to clean it up in a teardown, now it sticks
local function spy( name, trace )
-- build and return an appropriate function
return function( str, struct, meth )
local oldFunc = struct[meth]
struct[meth] = function(...)
-- @todo must be wrapped up and made html safe
stack[1+#stack] = { name, str, { "params", ... } }
if name == 'cluck' then
stackTrace( name )
elseif name == 'croak' then
error('croak')
end
return oldFunc(...)
end
end
end
-- @var This is the functions exposed for the tests
local members = {
describe = frame('describe'),
context = frame('context'),
it = frame('it'),
test = it,
pending = xframe('pending'),
xdescribe = xframe('xdescribe'),
xcontext = xframe('xcontext'),
xit = xframe('xit'),
xtest = xit,
-- after,
-- before,
expect = Expect,
carp = spy("carp"),
cluck = spy("cluck", true),
croak = spy("croak"),
confess = croak,
view = View,
result = function(head)
stack[2] = head
return wrap
end,
stack = function()
return stack
end,
similarity = function()
return similarity
end
}
return members
end
-- metatable for the export
local mt = {
-- adjust the installation of the module
__call = function ()
_G['_BDD_TEST'] = true
local t = BDD()
for k,v in pairs(t) do
_G[k] = v
end
return bdd
end
}
-- install the metatable
setmetatable(bdd, mt)
return bdd