You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lua-lemock/src/restrictions.nw

633 lines
18 KiB

Lua Easy Mock -- LeMock
Copyright (C) 2009 Tommy Pettersson <ptp@lysator.liu.se>
See terms in file COPYRIGHT, or at http://lemock.luaforge.net
@
Action Controll
###############
Times
=====
Some Actions might need to be replayed an arbitrary number of times,
although they should be replayed at least once. Others might be optional
and not need to be replayed at all.
The [[min_replays]] and [[max_replays]] properties of the Action implements
this kind of control. The Action is satisfied if it is replayed at least
[[min_replay]] times, but it will still continue to match on lookups until
it has been replayed [[max_replays]] times.
Common values for min and max:
+--------+-------------------+
| 1, 1 | exactly once |
+--------+-------------------+
| 1, inf | at least once |
+--------+-------------------+
| 0, 1 | no more than once |
+--------+-------------------+
| 0, inf | any times |
+--------+-------------------+
It is no harm to change the times more than once; they will simply be
overwritten. Suppressing multiple changes would require an extra control
property.
<<Unit test for module mock; times>>=
function times_test ()
local tmp = m.foo ;mc:returns( 2 ):times( 2, 3 )
mc:replay()
-- 1
local tmp = m.foo
local ok, err = pcall( function() mc:verify() end )
assert_false( ok, "verified unsatisfied action" )
assert_match( "Wrong replay count 1 ", err )
-- 2
local tmp = m.foo
mc:verify()
-- 3
local tmp = m.foo
mc:verify()
-- 4
local ok, err = pcall( function() local tmp = m.foo end )
assert_false( ok, "replaied finished action" )
assert_match( "Unexpected action index foo", err )
end
function times_called_twice_test ()
m.foo = 1 ;mc:times( 0, math.huge ):times( 1 )
end
function times_in_replay_mode_fails_test ()
mc:replay()
local ok, err = pcall( function() mc:times(1) end )
assert_false( ok, "changed times in replay mode" )
assert_match( "Can not set times in replay mode.", err )
end
function unrealistic_times_fails_with_message_test ()
m.a = 'a'
local ok, err = pcall( function() mc:times(0) end )
assert_false( ok, "accepted unrealistic time arguments" )
assert_match( "Unrealistic time arguments", err )
end
<<Class Controller method times>>=
function Controller:times (min, max)
if not self.is_recording then
error( "Can not set times in replay mode.", 0 )
end
self:get_last_action():set_times( min, max )
return self -- for chaining
end
-- convenience functions
function Controller:anytimes() return self:times( 0, math.huge ) end
function Controller:atleastonce() return self:times( 1, math.huge ) end
@
Action set_times
----------------
<<Unit test for class Action.generic method set_times>>=
function set_and_get_times_test ()
end
function unrealistic_times_fails_test ()
local ps = { {'foo'}, {8,'bar'}, {-1}, {3,2}, {1/0}, {0/0}, {0,0} }
for _, p in ipairs( ps ) do
local ok, err = pcall( function() a:set_times( unpack(p) ) end )
assert_false( ok, "unrealistic times "..table.concat(p,", ") )
assert_match( "Unrealistic time arguments ", err )
end
end
<<Class Action.generic method set_times>>=
function Action.generic:set_times (a, b)
min = a or 1
max = b or min
min, max = tonumber(min), tonumber(max)
if (not min) or (not max) or (min >= math.huge)
or (min ~= min) or (max ~= max) -- NaN
or (min < 0) or (max <= 0) or (min > max) then
error( sfmt( "Unrealistic time arguments (%s, %s)"
, qtostring( min )
, qtostring( max )
)
, 0
)
end
self.min_replays = min
self.max_replays = max
end
@
Label
=====
Labels help establishing relationships between Actions. The labels are
actually implemented as tags, in a many-to-many relationship, so that one
Action can have multiple labels, and the same label can be assigned to
multiple Actions.
<<Unit test for module mock; label>>=
function label_in_replay_mode_fails_test ()
mc:replay()
local ok, err = pcall( function() mc:label( 'foo' ) end )
assert_false( ok, "set label in replay mode" )
assert_match( "Can not add labels in replay mode", err )
end
function label_on_empty_actionlist_fails_test ()
local ok, err = pcall( function() mc:label( 'bar' ) end )
assert_false( ok, "set label with empty action list" )
assert_match( "No action is recorded yet", err )
end
<<Unit test for class Controller method label>>=
function label_test ()
mc:add_action( A:new() )
mc:label( 'a', 'b' ):label( 'c', 'b' )
local a = mc:get_last_action()
local seen = {}
for l in a:blocks() do
seen[l] = true
end
assert_true( seen['a'] )
assert_true( seen['b'] )
assert_true( seen['c'] )
assert_nil( seen['d'] )
end
<<Class Controller method label>>=
function Controller:label (...)
if not self.is_recording then
error( "Can not add labels in replay mode.", 2 )
end
local action = self:get_last_action()
for _, label in ipairs{ ... } do
action:add_label( label )
end
return self -- for chaining
end
@
Action add_label, has_label, and blocks
---------------------------------------
There is no method for iterating through the labels, but there is a
[[blocks]] method, that iterates through the labels if the Action is not
satisfied. This method is used for updating the dependency information.
<<Unit test for class Action.generic label methods>>=
function label_test ()
local ls = { 1/0, 0, false, {}, a, "foo", true }
for i = 1, #ls do
assert_false( a:has_label( ls[i] ))
end
for i = 1, #ls do
a:add_label( ls[i] )
for j = 1 , #ls do
if j <= i then
assert_true( a:has_label( ls[j] ))
else
assert_false( a:has_label( ls[j] ))
end
end
end
end
function add_label_twice_test ()
local l = 'foo'
a:add_label( l )
a:add_label( l )
local cnt = 0
for x in a:blocks() do
assert_equal( l, x )
cnt = cnt + 1
end
assert_equal( 1, cnt )
end
<<Class Action.generic method add_label>>=
function Action.generic:add_label (label)
add_to_set( self, 'labellist', label )
end
<<Class Action.generic method has_label>>=
function Action.generic:has_label (l)
for x in elements_of_set( self, 'labellist' ) do
if x == l then return true end
end
return false
end
<<Class Action.generic method blocks>>=
function Action.generic:blocks ()
if self:is_satisfied() then
return function () end
end
return elements_of_set( self, 'labellist' )
end
@
Depend
======
A typical usage case is expected to have some tenths actions, so there is
no need to optimize dependency calculations for speed. The dependencies are
stored as lists of labels in the action objects. When an action changes
states the Controller's [[update_dependency]] method examines the action
list. If an action is not satisfied all its labels are collected in a
blocking list, and finally all actions that depend on a label in the
blocking list are blocked by setting the [[is_blocked]] property to true.
<<Unit test for module mock; depend>>=
function depend_fulfilled_test ()
m.foo = 1 ;mc:label 'A'
m.bar = 2 ;mc:depend 'A'
mc:replay()
m.foo = 1
m.bar = 2
mc:verify()
end
function depend_unfulfilled_fails_test ()
m.foo = 1 ;mc:label 'A'
m.bar = 2 ;mc:depend 'A'
mc:replay()
local ok, err = pcall( function() m.bar = 2 end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action newindex", err )
end
function depend_fulfilled_any_order_test ()
local tmp
m.a = 1 ;mc:label 'A'
tmp = m.b ;mc:returns(2):depend 'A'
tmp = m.b ;mc:returns(3)
mc:replay()
assert_equal( 3, m.b, "replayed wrong b" )
m.a = 1
assert_equal( 2, m.b, "replayed wrong b" )
mc:verify()
end
function depend_serial_blocks_test ()
local tmp
tmp = m:a() ;mc:label 'a'
tmp = m:c() ;mc:label 'c' :depend 'b'
tmp = m:b() ;mc:label 'b' :depend 'a'
mc:replay()
local ok, err = pcall( function() tmp = m:b() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
local ok, err = pcall( function() tmp = m:c() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
m:a()
local ok, err = pcall( function() tmp = m:c() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
m:b()
m:c()
mc:verify()
end
function depend_on_many_labels_test ()
local tmp
tmp = m:b() ;mc:label 'b'
tmp = m:c() ;mc:label 'c' :depend( 'a', 'b' )
tmp = m:a() ;mc:label 'a'
mc:replay()
local ok, err = pcall( function() tmp = m:c() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
m:a()
local ok, err = pcall( function() tmp = m:c() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
m:b()
m:c()
mc:verify()
end
function depend_on_many_labels_test2_test ()
-- swap order, in case whole list is not checked
local tmp
tmp = m:b() ;mc:label 'b'
tmp = m:c() ;mc:label 'c' :depend( 'b', 'a' )
tmp = m:a() ;mc:label 'a'
mc:replay()
local ok, err = pcall( function() tmp = m:c() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
m:a()
local ok, err = pcall( function() tmp = m:c() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
m:b()
m:c()
mc:verify()
end
function depend_on_many_bloskers_with_same_label_test ()
tmp = m:c() ;mc:label 'c' :depend 'b'
tmp = m:a() ;mc:label 'b'
tmp = m:b() ;mc:label 'b'
mc:replay()
local ok, err = pcall( function() tmp = m:c() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
m:a()
local ok, err = pcall( function() tmp = m:c() end )
assert_false( ok, "replayed blocked action" )
assert_match( "Unexpected action", err )
m:b()
m:c()
mc:verify()
end
@
The algorithm implies that "unknown" labels are ignored, i.e. unless a
depended upon label is explicitly blocked by an unsatisfied action with
that a label, the dependency is considered fulfilled.
<<Unit test for module mock; depend>>=
function depend_ignors_unknown_label_test ()
m.foo = 1 ;mc:label 'A'
m.bar = 2 ;mc:depend 'B'
mc:replay()
m.foo = 1
m.bar = 2
mc:verify()
end
@
Cycles constitute a problem. A dependency cycle of actions will block
itself out and can not be replayed. Although this does not necessarily mean
replay will fail -- all the actions might for example have [[min_replays]]
set to zero, in which case they are automatically satisfied -- it is most
likely a user error to introduce a dependency cycle, and a helpful error
message is in place.
<<Unit test for module mock; depend>>=
function depend_detect_cycle_test ()
local ok, err = pcall( function()
m.foo = 1 ;mc:label 'A' :depend 'B'
m.bar = 2 ;mc:label 'B' :depend 'A'
mc:replay()
m.foo = 1
end )
assert_false( ok, "replayed cyclically blocked action" )
assert_match( "dependency cycle", err )
end
<<Unit test for module mock; depend>>=
function depend_chaining_test ()
m.a = 1 ;mc:label 'A'
m.b = 1 ;mc:label 'B'
m.c = 1 ;mc:depend('A'):depend('B')
end
function depend_in_replay_mode_fails_test ()
mc:replay()
local ok, err = pcall( function() mc:depend( 'foo' ) end )
assert_false( ok, "set dependency in replay mode" )
assert_match( "Can not add dependency in replay mode", err )
end
function depend_on_empty_actionlist_fails_test ()
local ok, err = pcall( function() mc:depend( 'bar' ) end )
assert_false( ok, "set dependency with empty action list" )
assert_match( "No action is recorded yet", err )
end
function depend_reports_expected_actions_on_faliure_test ()
local tmp
tmp = m.foo ;mc:depend 'B'
tmp = m.bar ;mc:label 'B'
mc:replay()
local ok, err = pcall( function() tmp = m.foo end )
assert_false( ok, "replayed blocked action" )
assert_match( "expected:.*index bar", err )
assert_not_match( "expected:.*index foo", err )
tmp = m.bar
local ok, err = pcall( function() tmp = m.bar end )
assert_false( ok, "expected:.*replayed blocked action" )
assert_not_match( "expected:.*index bar", err )
assert_match( "index foo", err )
end
@
Addind dependencies
-------------------
<<Class Controller method depend>>=
function Controller:depend (...)
if not self.is_recording then
error( "Can not add dependency in replay mode.", 2 )
end
local action = self:get_last_action()
for _, dependency in ipairs{ ... } do
action:add_depend( dependency )
end
return self -- for chaining
end
<<Unit test for Class Action.generic method add_depend and depends>>=
function add_depend_test ()
local ls = { 0, 'foo', 1/0, a, {} }
local seen = {}
for _, l in ipairs( ls ) do
seen[l] = 0
a:add_depend( l )
end
for l in a:depends() do
seen[l] = seen[l] + 1
end
for _, l in ipairs( ls ) do
assert_equal( 1, seen[l], "Mismatch for "..qtostring(l) )
end
end
function dependencies_dont_iterate_on_empty_list_test ()
for _ in a:depends() do
fail( "unexpected dependency" )
end
end
<<Class Action.generic method add_depend>>=
function Action.generic:add_depend (d)
add_to_set( self, 'dependlist', d )
end
<<Class Action.generic method depends>>=
function Action.generic:depends ()
return elements_of_set( self, 'dependlist' )
end
@
Updating dependencies and detecting cycles
------------------------------------------
<<Class Controller method update_dependencies>>=
function Controller:update_dependencies ()
local blocked = {}
for action in self:actions() do
for label in action:blocks() do
blocked[label] = true
end
end
local function is_blocked (action)
for label in action:depends() do
if blocked[label] then return true end
end
return false
end
for action in self:actions() do
action.is_blocked = is_blocked( action )
end
end
@
A cycle is detected by a complete depth-first search of the dependency
tree, examining for each step if new labels are already present somewhere
in the path to the new node. The design with multiple labels and multiple
dependencies makes the algorithm a bit extra complicated, because each node
in the tree is a set of labels.
A *node* is represented as a table, where labels are stored as array
elements, and the [[prev]] property references the previous node in the
*path*.
The use of temporary "linked lists" in this function generates a lot of
garbage for the garbage collector, but it is only run once.
<<Class Controller method assert_no_dependency_cycles>>=
function Controller:assert_no_dependency_cycles ()
local function is_in_path (label, path)
if not path then return false end -- is root
for _, l in ipairs( path ) do
if l == label then return true end
end
if path.prev then return is_in_path( label, path.prev ) end
return false
end
local function can_block (action, node)
for _, label in ipairs( node ) do
if action:has_label( label ) then return true end
end
return false
end
local function step (action, path)
local new_head
for label in action:depends() do
if is_in_path( label, path ) then
error( "Detected dependency cycle", 0 )
end
-- only create table if needed to reduce garbage
if not new_head then new_head = { prev=path } end
new_head[#new_head+1] = label
end
return new_head
end
local function search_depth_first (path)
for action in self:actions() do
if can_block( action, path ) then
local new_head = step( action, path )
if new_head then
search_depth_first( new_head )
end
end
end
end
for action in self:actions() do
local root = step( action, nil )
if root then search_depth_first( root ) end
end
end
@
Close
=====
<<Unit test for module mock; close>>=
function close_test ()
local t
t = m.foo ;mc:times(0,1/0):returns( 1 ) :label(1)
t = m.foo ;mc:times(0,1/0):returns( 2 ) :label(2)
t = m.foo ;mc:times(0,1/0):returns( 3 )
m.bar(1) ;mc:close(1)
m.bar(2) ;mc:close(2)
mc:replay()
m.bar(1)
assert_equal( 2, m.foo )
assert_equal( 2, m.foo )
assert_equal( 2, m.foo )
m.bar(2)
assert_equal( 3, m.foo )
mc:verify()
end
function close_unsatisfied_action_fails_test ()
m.a = 1 ;mc:label(1)
m.b = 2 ;mc:close(1)
mc:replay()
local ok, err = pcall( function() m.b = 2 end )
assert_false( ok, "Undetected close of unsatisfied action" )
assert_match( "Closes unsatisfied action", err )
end
function close_multiple_test ()
m.foo(1) ;mc:label(1) :times(0,1)
m.foo(1) ;mc:label(2) :times(0,1)
m.foo(1)
m.bar() ;mc:close(1,2)
mc:replay()
m.bar()
m.foo(1)
mc:verify()
end
<<Unit test for module mock; close>>=
function close_chaining_test ()
m.a = 1 ;mc:label 'A'
m.b = 1 ;mc:label 'B'
m.c = 1 ;mc:close('A'):close('B')
end
function close_in_replay_mode_fails_test ()
mc:replay()
local ok, err = pcall( function() mc:close( 'foo' ) end )
assert_false( ok, "accepted close in replay mode" )
assert_match( "Can not insert close in replay mode", err )
end
function close_on_empty_actionlist_fails_test ()
local ok, err = pcall( function() mc:close( 'bar' ) end )
assert_false( ok, "accepted close with empty action list" )
assert_match( "No action is recorded yet", err )
end
@
Adding closes
-------------
<<Class Controller method close>>=
function Controller:close (...)
if not self.is_recording then
error( "Can not insert close in replay mode.", 2 )
end
local action = self:get_last_action()
for _, close in ipairs{ ... } do
action:add_close( close )
end
return self -- for chaining
end
<<Class Action.generic method add_close>>=
function Action.generic:add_close (label)
add_to_set( self, 'closelist', label )
end
@
Perform closes
--------------
<<Class Controller method close_actions>>=
function Controller:close_actions( ... ) -- takes iterator
for label in ... do
for candidate in self:actions() do
if candidate:has_label( label ) then
if not candidate:is_satisfied() then
error( "Closes unsatisfied action: "..candidate:tostring(), 0 )
end
candidate.is_closed = true
end
end
end
end
<<Class Action.generic method closes>>=
function Action.generic:closes ()
return elements_of_set( self, 'closelist' )
end