From 2c9b8a671d2f3a95bdf13bb8894c88fc82bc703a Mon Sep 17 00:00:00 2001 From: Victor Seva Date: Wed, 20 Mar 2013 15:14:35 +0100 Subject: [PATCH] Imported Upstream version 0.6 --- .travis.yml | 51 ++ CMakeLists.txt | 14 + COPYRIGHT | 28 + DEVEL | 57 ++ HISTORY | 22 + README | 44 ++ build/htdocs/COPYRIGHT.html | 58 ++ build/htdocs/DEVEL.html | 98 +++ build/htdocs/HISTORY.html | 59 ++ build/htdocs/README.html | 69 ++ build/htdocs/index.html | 26 + build/htdocs/style.css | 120 +++ build/htdocs/userguide.html | 425 ++++++++++ build/lemock.lua | 659 ++++++++++++++++ build/mkfile | 20 + build/unit/action.lua | 603 +++++++++++++++ build/unit/action_generic.lua | 598 ++++++++++++++ build/unit/argv.lua | 213 +++++ build/unit/controller.lua | 860 +++++++++++++++++++++ build/unit/module.lua | 601 ++++++++++++++ build/unit/userguide.lua | 264 +++++++ build/userguide.t2t | 352 +++++++++ build/www/COPYRIGHT.t2t | 11 + build/www/DEVEL.t2t | 13 + build/www/HISTORY.t2t | 13 + build/www/README.t2t | 13 + build/www/config.rc | 3 + build/www/index.t2t | 9 + build/www/menubar.html | 7 + build/www/userguide.t2t | 13 + cmake/FindLua.cmake | 118 +++ cmake/dist.cmake | 321 ++++++++ cmake/lua.cmake | 293 +++++++ dist.info | 14 + src/action/call.nw | 122 +++ src/action/generic_call.nw | 80 ++ src/action/index.nw | 136 ++++ src/action/newindex.nw | 109 +++ src/action/selfcall.nw | 95 +++ src/argv.nw | 158 ++++ src/class/action.nw | 105 +++ src/class/argv.nw | 11 + src/class/callable.nw | 13 + src/class/controller.nw | 30 + src/class/mock.nw | 13 + src/doc/userguide/chapter_controller.nw | 23 + src/doc/userguide/chapter_introduction.nw | 87 +++ src/doc/userguide/chapter_mock.nw | 21 + src/doc/userguide/chapter_tricks.nw | 67 ++ src/doc/userguide/main.nw | 23 + src/doc/userguide/section_actions.nw | 34 + src/doc/userguide/section_anyargs.nw | 57 ++ src/doc/userguide/section_close.nw | 64 ++ src/doc/userguide/section_label_depend.nw | 69 ++ src/doc/userguide/section_returns_error.nw | 38 + src/doc/userguide/section_times.nw | 65 ++ src/doc/userguide/unittests.nw | 19 + src/doc/webpages.nw | 297 +++++++ src/files.nw | 32 + src/helperfunctions.nw | 65 ++ src/main.nw | 761 ++++++++++++++++++ src/misc.nw | 30 + src/restrictions.nw | 632 +++++++++++++++ src/tostring.nw | 182 +++++ src/unittestfiles.nw | 169 ++++ tools/autotangle | 28 + 66 files changed, 9704 insertions(+) create mode 100644 .travis.yml create mode 100644 CMakeLists.txt create mode 100644 COPYRIGHT create mode 100644 DEVEL create mode 100644 HISTORY create mode 100644 README create mode 100644 build/htdocs/COPYRIGHT.html create mode 100644 build/htdocs/DEVEL.html create mode 100644 build/htdocs/HISTORY.html create mode 100644 build/htdocs/README.html create mode 100644 build/htdocs/index.html create mode 100644 build/htdocs/style.css create mode 100644 build/htdocs/userguide.html create mode 100644 build/lemock.lua create mode 100644 build/mkfile create mode 100644 build/unit/action.lua create mode 100644 build/unit/action_generic.lua create mode 100644 build/unit/argv.lua create mode 100644 build/unit/controller.lua create mode 100644 build/unit/module.lua create mode 100644 build/unit/userguide.lua create mode 100644 build/userguide.t2t create mode 100644 build/www/COPYRIGHT.t2t create mode 100644 build/www/DEVEL.t2t create mode 100644 build/www/HISTORY.t2t create mode 100644 build/www/README.t2t create mode 100644 build/www/config.rc create mode 100644 build/www/index.t2t create mode 100644 build/www/menubar.html create mode 100644 build/www/userguide.t2t create mode 100644 cmake/FindLua.cmake create mode 100644 cmake/dist.cmake create mode 100644 cmake/lua.cmake create mode 100644 dist.info create mode 100644 src/action/call.nw create mode 100644 src/action/generic_call.nw create mode 100644 src/action/index.nw create mode 100644 src/action/newindex.nw create mode 100644 src/action/selfcall.nw create mode 100644 src/argv.nw create mode 100644 src/class/action.nw create mode 100644 src/class/argv.nw create mode 100644 src/class/callable.nw create mode 100644 src/class/controller.nw create mode 100644 src/class/mock.nw create mode 100644 src/doc/userguide/chapter_controller.nw create mode 100644 src/doc/userguide/chapter_introduction.nw create mode 100644 src/doc/userguide/chapter_mock.nw create mode 100644 src/doc/userguide/chapter_tricks.nw create mode 100644 src/doc/userguide/main.nw create mode 100644 src/doc/userguide/section_actions.nw create mode 100644 src/doc/userguide/section_anyargs.nw create mode 100644 src/doc/userguide/section_close.nw create mode 100644 src/doc/userguide/section_label_depend.nw create mode 100644 src/doc/userguide/section_returns_error.nw create mode 100644 src/doc/userguide/section_times.nw create mode 100644 src/doc/userguide/unittests.nw create mode 100644 src/doc/webpages.nw create mode 100644 src/files.nw create mode 100644 src/helperfunctions.nw create mode 100644 src/main.nw create mode 100644 src/misc.nw create mode 100644 src/restrictions.nw create mode 100644 src/tostring.nw create mode 100644 src/unittestfiles.nw create mode 100644 tools/autotangle diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..049d304 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,51 @@ +# +# LuaDist Travis-CI Hook +# + +# We assume C build environments +language: C + +# Try using multiple Lua Implementations +env: + - TOOL="" # Use native compiler (GCC usually) + - COMPILER="clang" # Use clang + - TOOL="i686-w64-mingw32" # 32bit MinGW + - TOOL="x86_64-w64-mingw32" # 64bit MinGW + - TOOL="arm-linux-gnueabihf" # ARM hard-float (hf), linux + +# Crosscompile builds may fail +matrix: + allow_failures: + - env: TOOL="i686-w64-mingw32" + - env: TOOL="x86_64-w64-mingw32" + - env: TOOL="arm-linux-gnueabihf" + +# Install dependencies +install: + - git clone git://github.com/LuaDist/_util.git ~/_util + - ~/_util/travis install + +# Bootstap +before_script: + - ~/_util/travis bootstrap + +# Build the module +script: + - ~/_util/travis build + +# Execute additional tests or commands +#after_script: +# - ~/_util/travis test + +# Only watch the master branch +branches: + only: + - master + +# Notify the LuaDist Dev group if needed +notifications: + recipients: + - luadist-dev@googlegroups.com + email: + on_success: change + on_failure: always \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3d3fbf1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +# Copyright (C) 2011-2012 LuaDist. +# Created by Peter Kapec +# Redistribution and use of this file is allowed according to the terms of the MIT license. +# For details see the COPYRIGHT file distributed with LuaDist. +# Please note that the package source code is licensed under its own license. + +project ( lemock NONE ) +cmake_minimum_required ( VERSION 2.8 ) +include ( cmake/dist.cmake ) +include ( lua ) + +# Install all files and documentation +install_lua_module ( lemock build/lemock.lua ) +install_doc ( build/htdocs/ ) diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..6714417 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,28 @@ +LeMock License + + +LeMock is licensed under the terms of the MIT license reproduced below. This +means that LeMock is free software and can be used for both academic and +commercial purposes at absolutely no cost. + +-------------------------------------------------------------------------- +Copyright (C) 2009 Tommy Petterson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +-------------------------------------------------------------------------- diff --git a/DEVEL b/DEVEL new file mode 100644 index 0000000..a8c5906 --- /dev/null +++ b/DEVEL @@ -0,0 +1,57 @@ +LeMock Developer Notes + + +LeMock is implemented in Lua 5.1, but its source code is written as +literate documents, and usees the tool noweb to generate the .lua files. +The distributed source archive includes all the tangled files in the build +directory, to avoid dependending on noweb for installation. + +The source is contained in the src directory in the form of literate +documents. To extract (tangle) the final files from the sources, the tool +[noweb http://www.cs.tufts.edu/~nr/noweb/] is needed. + +The source files are meant to be tangled together all at once, because the +contents of a target file can be spread over several source files. The +target files are all the chunk names that are roots. These can be found +with noroots in the noweb toolbox. To automate the tangle process, there is +a custom script named autotangle in the tools directory. This script finds +all the target file names and tangles them in the current directory, +creating subdirectories as needed. The script is written for yet another +obscure tool, [rc http://www.libra-aries-books.co.uk/software/rc] [1]. It +is probably easy to port the short rc script to your favourit language. It +is invoked (in rc syntax) as: + +``` ../tools/autotangle `{find ../src -name '*.nw'} + +which means it wants all (recursively) .nw files in the src directory as +arguments. + +The documentation is written in [txt2tags http://txt2tags.sourceforge.net/], +which can generate HTML among many other formats. The README, HISTORY, +COPYRIGHT, and this DEVEL text file are written as simple txt2tags +documents to remain readable as is. The .nw source files define wrapper +txt2tags documents for the web pages, which use txt2tags' include mechanism +to include the actual txt2tags files. The user guide is defined in the .nw +sources as a separate txt2tags document, so it can be easily generated as a +LaTeX document or Unix man page, but it too is included in a wrapper +txt2tags document when generating the web pages. + +The building of the web pages is done with +[mk http://en.wikipedia.org/wiki/Mk_%28software%29], a Unix port of the +Plan9 make tool. (The mk files uses rc syntax.) + +A set of unit tests (defined in the .nw source) can be run with +[lunit http://www.nessie.de/mroth/lunit/index.html] with the command: + +``` lunit unit/*.lua + +A program like [luacov http://luacov.luaforge.net/] can be used to check +the coverage of the unit tests. + + +-------------------- +== Footnotes == +: [1] + I use Byron's Unix port, which syntax is extended and incompatible with + the original (and other ports). +: diff --git a/HISTORY b/HISTORY new file mode 100644 index 0000000..8a6c383 --- /dev/null +++ b/HISTORY @@ -0,0 +1,22 @@ +LeMock History + + +: 0.6 + - Actions can be set to raise an error. + - //anytimes// and //atleastonce//. + - Fail immediately if an unsatisfied action is closed. + - Simplify semantics for Anyarg and Anyargs. + - Documentation. +: 0.5 + - Anyarg and Anyargs. + - Third rewrite (major refactoring). +: 0.4 + - Labels, dependencies, closes, and replay count limits. + - Allow attribute modifying controller methods to be chained. +: 0.3 + - Initial import. + - This is the second rewrite of the initial prototype. It handles recording + and replaying of actions with returnvalues, and verifies replay + completeness. + + diff --git a/README b/README new file mode 100644 index 0000000..1df32fa --- /dev/null +++ b/README @@ -0,0 +1,44 @@ +LeMock Readme + + +== What is Lua Easy Mock == + +LeMock (Lua Easy Mock) is a mock creation module intended for use together +with a unit test framework such as lunit or lunity. It is inspired by +EasyMock (for Java), and strives to be easy to use. + + +== Availability == + +LeMock is hosted at LuaForge at http://luaforge.net/projects/lemock/ + + +== Installation == + +Copy the file build/lemock.lua to a Lua search path directory. This is +usually something like ``/usr/share/lua/5.1/`` or +``/usr/local/lib/lua/5.1/``. You can type ``print(package.path)`` at the +Lua prompt to see what search path your Lua installation is using. + +Documentation in HTML format is available in build/htdocs/. + + +== License == + +LeMock is licensed under the MIT license, which is the same license that +Lua uses. See COPYRIGHT_ for full terms. + + +== User Documentation == + +See build/htdocs/userguide.html. + + +== Development == + +LeMock is implemented in Lua 5.1, but its source code is written as +literate documents, and uses the tool noweb to tangle the .lua files. The +distributed source archive includes all the tangled files, to avoid +depending on noweb for installation. See DEVEL_ for information about how +the source code is organized, and what tools are needed for the build +process. diff --git a/build/htdocs/COPYRIGHT.html b/build/htdocs/COPYRIGHT.html new file mode 100644 index 0000000..1ec776e --- /dev/null +++ b/build/htdocs/COPYRIGHT.html @@ -0,0 +1,58 @@ + + + + + +LeMock + + + + + + + + + + diff --git a/build/htdocs/DEVEL.html b/build/htdocs/DEVEL.html new file mode 100644 index 0000000..13ccad2 --- /dev/null +++ b/build/htdocs/DEVEL.html @@ -0,0 +1,98 @@ + + + + + +LeMock + + + + + +
+ +

Developer Notes

+

+LeMock is implemented in Lua 5.1, but its source code is written as +literate documents, and usees the tool noweb to generate the .lua files. +The distributed source archive includes all the tangled files in the build +directory, to avoid dependending on noweb for installation. +

+

+The source is contained in the src directory in the form of literate +documents. To extract (tangle) the final files from the sources, the tool +noweb is needed. +

+

+The source files are meant to be tangled together all at once, because the +contents of a target file can be spread over several source files. The +target files are all the chunk names that are roots. These can be found +with noroots in the noweb toolbox. To automate the tangle process, there is +a custom script named autotangle in the tools directory. This script finds +all the target file names and tangles them in the current directory, +creating subdirectories as needed. The script is written for yet another +obscure tool, rc [1]. It +is probably easy to port the short rc script to your favourit language. It +is invoked (in rc syntax) as: +

+
+../tools/autotangle `{find ../src -name '*.nw'}
+
+

+

+which means it wants all (recursively) .nw files in the src directory as +arguments. +

+

+The documentation is written in txt2tags, +which can generate HTML among many other formats. The README, HISTORY, +COPYRIGHT, and this DEVEL text file are written as simple txt2tags +documents to remain readable as is. The .nw source files define wrapper +txt2tags documents for the web pages, which use txt2tags' include mechanism +to include the actual txt2tags files. The user guide is defined in the .nw +sources as a separate txt2tags document, so it can be easily generated as a +LaTeX document or Unix man page, but it too is included in a wrapper +txt2tags document when generating the web pages. +

+

+The building of the web pages is done with +mk, a Unix port of the +Plan9 make tool. (The mk files uses rc syntax.) +

+

+A set of unit tests (defined in the .nw source) can be run with +lunit with the command: +

+
+lunit unit/*.lua
+
+

+

+A program like luacov can be used to check +the coverage of the unit tests. +

+
+

Footnotes

+
+
[1]
+ I use Byron's Unix port, which syntax is extended and incompatible with + the original (and other ports). +
+ +
+

+2009-05-31 +

+
+ + + + diff --git a/build/htdocs/HISTORY.html b/build/htdocs/HISTORY.html new file mode 100644 index 0000000..79089d4 --- /dev/null +++ b/build/htdocs/HISTORY.html @@ -0,0 +1,59 @@ + + + + + +LeMock + + + + + +
+ +

History

+
+
0.6
+
    +
  • Actions can be set to raise an error. +
  • anytimes and atleastonce. +
  • Fail immediately if an unsatisfied action is closed. +
  • Simplify semantics for Anyarg and Anyargs. +
  • Documentation. +
+
0.5
+
    +
  • Anyarg and Anyargs. +
  • Third rewrite (major refactoring). +
+
0.4
+
    +
  • Labels, dependencies, closes, and replay count limits. +
  • Allow attribute modifying controller methods to be chained. +
+
0.3
+
    +
  • Initial import. +
  • This is the second rewrite of the initial prototype. It handles recording + and replaying of actions with returnvalues, and verifies replay + completeness. +
+
+ +
+

+2009-05-31 +

+
+ + + + diff --git a/build/htdocs/README.html b/build/htdocs/README.html new file mode 100644 index 0000000..adec969 --- /dev/null +++ b/build/htdocs/README.html @@ -0,0 +1,69 @@ + + + + + +LeMock + + + + + +
+ +

Readme

+

What is Lua Easy Mock

+

+LeMock (Lua Easy Mock) is a mock creation module intended for use together +with a unit test framework such as lunit or lunity. It is inspired by +EasyMock (for Java), and strives to be easy to use. +

+

Availability

+

+LeMock is hosted at LuaForge at http://luaforge.net/projects/lemock/ +

+

Installation

+

+Copy the file build/lemock.lua to a Lua search path directory. This is +usually something like /usr/share/lua/5.1/ or +/usr/local/lib/lua/5.1/. You can type print(package.path) at the +Lua prompt to see what search path your Lua installation is using. +

+

+Documentation in HTML format is available in build/htdocs/. +

+

License

+

+LeMock is licensed under the MIT license, which is the same license that +Lua uses. See COPYRIGHT for full terms. +

+

User Documentation

+

+See the user guide. +

+

Development

+

+LeMock is implemented in Lua 5.1, but its source code is written as +literate documents, and uses the tool noweb to tangle the .lua files. The +distributed source archive includes all the tangled files, to avoid +depending on noweb for installation. See DEVEL for information about how +the source code is organized, and what tools are needed for the build +process. +

+
+

+2009-05-31 +

+
+ + + + diff --git a/build/htdocs/index.html b/build/htdocs/index.html new file mode 100644 index 0000000..d4a04f2 --- /dev/null +++ b/build/htdocs/index.html @@ -0,0 +1,26 @@ + + + + + +LeMock + + + + + + + + + + diff --git a/build/htdocs/style.css b/build/htdocs/style.css new file mode 100644 index 0000000..52c88f5 --- /dev/null +++ b/build/htdocs/style.css @@ -0,0 +1,120 @@ + body { + color: #181818; + background-color: #E0E4F0; + font: normal 10pt sans-serif; + max-width: 30em; + margin: 25pt; + } + .body h1 { + margin: 2em 0em 0em 0em; + font-size: 14pt; + } + .body h2 { + margin: 1.5em 0em 0em 0em; + font-size: 12pt; + } + .body h3 { + margin: 1em 0em 0em 0em; + font-size: 10pt; + } + .body p, .body ul, .body ol { + margin-top: 0.5em; + } + .body li { + margin-top: 0.5em; + } + a { + text-decoration: none; + } + hr { + margin-top: 3em; + } + + .header h1 { + text-align: center; + padding: 0.3em; + border: 1pt solid black; + } + + code, pre { + font-family: fixed; + font-style: normal; + font-size: 9pt; + line-height: 9pt; + background-color: #E8ECF8; + } + pre { + padding: 2pt; + } + + div.toc { + margin-top: 3em; + line-height: 6pt; + } + .toc ul { + padding-left: 1.6em; + margin: 0em; + line-height: 10pt; + } + .toc li { + margin: 0em; + padding: 0em; + list-style-type: none; + } + .toc a { + color: #091; + } + + #page-HISTORY DL DT { + font-weight: bold; + margin-top: 2em; + } + #page-HISTORY DL DD UL { + margin-top: 0pt; + padding-left: 0pt; + } + + #main_menu { + margin: 0; + padding: 0; + } + #main_menu li { + margin: 0; + padding: 0; + display: inline; + } + #main_menu a { + padding: 3px 3px 2px 4px; + text-decoration:none; + font:bold 8pt/8pt Arial, Helvetica, sans-serif; + border: 1px solid #000; + } + #main_menu a:link, + #main_menu a:visited { + color: #fff; + background: #777; + } + #main_menu a:hover { + color: #000; + background: #777; + } + #page-README #main_menu-README a, + #page-COPYRIGHT #main_menu-COPYRIGHT a, + #page-userguide #main_menu-userguide a, + #page-HISTORY #main_menu-HISTORY a, + #page-DEVEL #main_menu-DEVEL a { + color: #000; + background: #aaa; + } + #page-README #main_menu-README a:hover, + #page-COPYRIGHT #main_menu-COPYRIGHT a:hover, + #page-userguide #main_menu-userguide a:hover, + #page-HISTORY #main_menu-HISTORY a:hover, + #page-DEVEL #main_menu-DEVEL a:hover { + color: #000; + background: #aaa; + } + #nav a:active { + color: #000; + background: #aaa; + } diff --git a/build/htdocs/userguide.html b/build/htdocs/userguide.html new file mode 100644 index 0000000..ae13153 --- /dev/null +++ b/build/htdocs/userguide.html @@ -0,0 +1,425 @@ + + + + + +LeMock + + + + + +
+ + + +

Introduction

+

+Mock objects replace difficult external objects during unit testing by +simulating the behaviors of the replaced objects. This is done by first +recording actions and their responses with the mock objects, and then +switching to replay mode. During replay mode the mock objects simulate the +replaced objects by looking up actions and replaying the recorded +responses, and finally verifying that all expected actions where completely +replayed. +

+

+Actions are stored in a list in a special controller object. During replay +the list is searched in recording order for the first matching action that +can be replayed. +

+

+Restrictions on the actions can be inserted during the recording phase. An +action can have a maximum count of how many times it will be replayed, and +a minimum count of how many times it must be replayed to be satisfied. An +action can depend on any set of other actions, and can not be replayed +before all of its depended actions are satisfied. An action can close any +set of actions when it is replayed, which stops all further replaying of +the closed actions. This is good for simulating state changes. +

+

Example

+

+This example tests that the insert_data function of the foo module handles +a missing data base table gracefully. +

+
+-- Setup
+require 'lemock'
+local mc = lemock.controller()
+local sqlite3 = mc:mock()
+local env     = mc:mock()
+local con     = mc:mock()
+package.loaded.luasql = nil
+package.preload['luasql.sqlite3'] = function ()
+    luasql = {}
+    luasql.sqlite3 = sqlite3
+    return sqlite3
+end
+
+-- Record
+sqlite3()                 ;mc :returns(env)
+env:connect('/data/base') ;mc :returns(con)
+con:execute(mc.ANYARGS)   ;mc :error('LuaSQL: no such table')
+con:close()
+env:close()
+
+-- Replay
+mc:replay()
+require 'foo'
+local res = foo.insert_data(17)
+assert(res==false)
+
+--Verify
+mc:verify()
+
+

+

+First a controller is created. Then three mock objects are created, one for +the sqlite3 module, and two for objects returned by the (simulated) module. +

+

+Then a preloader for the sqlite3 module is installed, which returns the +sqlite3 mock object instead of the actual sqlite3 module. +

+

+In the record phase the expected calls and their return values (or thrown +errors) are recorded. The order is not significant, so this simplified test +will not detect if the close method is called before the execute method. +

+

+In the replay phase the tested module is loaded and executed. It will use +the mock objects instead of the real data base, and if it makes any +unrecorded calls, an error is thrown. +

+

+The verify phase asserts that all recorded actions have been replayed. If +the foo module for example forgets to call the close method, verify throws +an error. +

+ +

The Mock Object

+

+Mock objects are empty objects with special Lua meta methods that detect +actions performed with the object. What happens depends on the state +(recording or replaying) of the controller which created the mock object. +During recording the mock object adds the action to the controller's list +of recorded actions. During replay the mock object looks for a matching +recorded action that can be replayed, and simulates the action. +

+

+Some action attributes can not be inferred by the mock objects, for example +return values. These attributes have to be added afterwards with special +controller methods, and always affect the last recorded action. +

+ +

Actions

+

+Mock objects detect four types of actions: assignment, indexing, method +call, and self call. During replay an action will only match if it is the +very same action, that is, the same type of action performed on the same +mock object with all the same arguments. There are however +special arguments that can be used during recording. +

+
+require 'lemock'
+local mc = lemock.controller()
+local m = mc:mock()
+
+m.x = 17    -- assignment
+r = m.x     -- indexing
+m.x(1,2,3)  -- method call
+m:x(1,2,3)  -- method call
+m(1,2,3)    -- self call
+
+ +

Anyargs

+

+An anyarg is a special argument used when recording, that will match +any argument during replay. It can appear anywhere and any times in an +argument list, or as the argument in an assignment, to replace real +arguments. There is also anyargs, which will match any number +(including zero) of any arguments. Anyargs can only appear as the last +argument of an argument list. Anyarg and anyargs are handy when the actual +values of the arguments during replay are unimportant or unknown. +

+

+Anyarg and anyargs are constants defined in the controller object. +

+

Example

+

+This example tests that the fetch_data function of module foo waits a while +and retries when no data is immediately available, and that it updates the +value of lasttime. +

+
+require 'lemock'
+local mc = lemock.controller()
+local con = mc:mock()
+
+con:poll()           ;mc :returns(nil)
+con:sleep(mc.ANYARG)
+con:poll()           ;mc :returns('123.45')
+con.lasttime = mc.ANYARG
+
+mc:replay()
+require 'foo'
+local res = foo.fetch_data(con)
+assert( math.abs(res-123.45) < 0.0005 )
+
+mc:verify()
+
+ +

The Controller

+

+The controller's main purpose is to store the recorded actions, create mock +objects, switch to replay mode, and verify the completion of the replay +phase. But it is also needed to set or change special action attributes +during recording. +

+

+It is possible, although doubtfully useful, to use several controllers in +parallel during a single unit test. Each controller maintains its own +action list and state, and mock objects remember which controller they +belong to. +

+ +

Returns & Error

+

+The by far most useful special action attribute is the return value. +Indexing actions can return a single value, while call actions and self +call actions can return a list of values. The return value is set with the +returns method, and it is an error to set the return value twice for +the same action. +

+

+For purposes of unit testing it is often useful to simulate errors. All +actions can raise an error, and return an error value (usually a string). +The return value is set with the error method. An action can not have +both a return value and raise an error. +

+

Example

+
+require 'lemock'
+local mc = lemock.controller()
+local m = mc:mock()
+
+m:foo(17)  ;mc :returns(nil, "index out of range")
+m:bar(-1)  ;mc :error("invalid index")
+
+ +

Label & Depend

+

+Dependencies block actions from replaying until other actions have replayed +first. They can be used to verify that actions are being replayed in a +valid order. +

+

+To add dependencies, actions must first be labeled with one or more +labels. The same label can be given to several actions. As long as some +action with the label remains unsatisfied, that label is blocked, and all +actions depending on that label will not replay. +

+

Example

+

+This (contrived) example tests that function draw_square in module foo +calls all the necessary drawing methods of a square object in a correct +order. Note that there can be more than one correct order. +

+
+require 'lemock'
+local mc = lemock.controller()
+local square = mc:mock()
+
+square:topleft()   ;mc :label('tl')
+square:topright()  ;mc :label('tr')
+square:botleft()   ;mc :label('bl')
+square:botright()  ;mc :label('br')
+square:leftedge()  ;mc :label('edge') :depend('tl', 'bl')
+square:rightedge() ;mc :label('edge') :depend('tr', 'br')
+square:topedge()   ;mc :label('edge') :depend('tl', 'tr')
+square:botedge()   ;mc :label('edge') :depend('bl', 'br')
+square:fill()      ;mc                :depend('edge')
+
+mc:replay()
+require 'foo'
+foo.draw_square( square )
+
+mc:verify()
+
+

+

+This example demonstrates two different ways of using dependencies. All the +corners have unique labels, because each edge depend on a set of specific +corners. But all the edges have the same label, because the fill operation +only depends on all edges have been satisfied. +

+ +

Times

+

+The default for a recorded action is to be replayed exactly once. +times(2) changes that to exactly two times, and times(1,2) changes +it to at least one time and at most two times. +

+

+When the action has been replayed the least count times it is +satisfied, which means verify will not complain about it, and it no +longer blocks actions that depend on this action from being replayed. If +the least count is zero the action is automatically satisfied and need not +be replayed at all, i.e., it is optional. +

+

+When the action has been replayed the most count times it will not replay +any more. The most replay count can be set to infinity (math.huge or +1/0), in which case the action will never stop replaying. +

+

+anytimes() can be used as an alias for times(0,1/0), and +atleastonce() can be used as an alias for times(1,1/0). +

+

Example

+

+This example tests that method update is called at least once. +

+
+require 'lemock'
+local mc = lemock.controller()
+local con = mc:mock()
+
+con:log(mc.ANYARGS) ;mc                :anytimes()
+con:update('x',3)   ;mc :returns(true) :atleastonce()
+
+mc:replay()
+require 'foo'
+local watcher = foo.mk_watcher( con )
+watcher:set( 'x', 3 )
+
+mc:verify()
+
+ +

Close

+

+Close can be used to simulate state changes in a limited way. When an +action with a close statement is replayed for the first time, it will +permanently block all labels in its close statement, so that actions with +these labels no longer replays. This passes on matching to later actions in +the action list, which may for example have different return values. +

+

+The closing simply blocks the labels, and it has nothing to do with max +replay counts or if closed actions have been satisfied or not. Closing an +unsatisfied action however results in an immediate failure. +

+

Example

+

+This example tests that the dump function of module foo calls the myio +functions in a correct order. The read function can be called any number of +times, until it is closed by the close function. +

+
+require 'lemock'
+local mc = lemock.controller()
+local myio = mc:mock()
+local fs   = mc:mock()
+
+myio.open('abc', 'r') ;mc :returns(fs)
+mc :label('open')
+
+fs:read(mc.ANYARG) ;mc :returns('data')
+mc :atleastonce() :label('read') :depend('open')
+
+fs:close() ;mc :returns(true)
+mc :depend('open') :close('read')
+
+mc:replay()
+require 'foo'
+foo.dump(myio, 'abc', 128)
+
+mc:verify()
+
+ +

Tricks

+

+Mock objects are completely empty, and do not contain any methods or +properties of their own. If they did, that would risk shadowing a name of a +simulated object's method or property. There is however nothing preventing +users from defining methods and properties in mock objects. This way mock +objects can be turned into stubs, or a kind of mock–stub hybrid. +

+ +

Method Overloading

+

+Lua does not support method overloading, but it can be (and sometimes is) +implemented manually by testing of function arguments. This presents a +problem to LeMock, because it matches exact arguments, and anyargs in not +sufficient. In this case the mock object can be extended with a dispatcher +function. +

+

Example

+

+This example shows a mock object with an overloaded add function. The stub +function can not be defined in the usual way, because that would record an +assignment action; it needs to be defined with rawset. +

+
+require 'lemock'
+local mc = lemock.controller()
+local m = mc:mock()
+
+do
+local function add (a, b)
+    if type(a) == 'number' then
+        return m.add_number(a, b)
+    else
+        return m.add_string(a, b)
+    end
+end
+rawset( m, 'add', add ) -- not recorded
+end -- do
+
+m.add_number(1, 2)         ;mc :returns(3)
+m.add_string('foo', 'bar') ;mc :returns('foobar')
+
+mc:replay()
+assert_equal( 3, m.add(1, 2) )
+assert_equal( 'foobar', m.add('foo', 'bar') )
+
+mc:verify()
+
+
+

+2009-05-31 +

+
+ + + + diff --git a/build/lemock.lua b/build/lemock.lua new file mode 100644 index 0000000..a25bc8c --- /dev/null +++ b/build/lemock.lua @@ -0,0 +1,659 @@ +------ THIS FILE IS TANGLED FROM LITERATE SOURCE FILES ------ +-- Copyright (C) 2009 Tommy Pettersson +-- See terms in file COPYRIGHT, or at http://lemock.luaforge.net +module( 'lemock', package.seeall ) +_VERSION = "LeMock 0.6" +_COPYRIGHT = "Copyright (C) 2009 Tommy Pettersson " +local class, object, qtostring, sfmt, add_to_set +local elements_of_set, value_equal +function object (class) + return setmetatable( {}, class ) +end +function class (parent) + local c = object(parent) + c.__index = c + return c +end +sfmt = string.format +function qtostring (v) + if type(v) == 'string' then + return sfmt( '%q', v ) + else + return tostring( v ) + end +end +function add_to_set (o, setname, element) + if not o[setname] then + o[setname] = {} + end + local l = o[setname] + for i = 1, #l do + if l[i] == element then return end + end + l[#l+1] = element +end +function elements_of_set (o, setname) + local l = o[setname] + local i = l and #l+1 or 0 + return function () + i = i - 1 + if i > 0 then return l[i] end + end +end +function value_equal (a, b) + if a == b then return true end + if a ~= a and b ~= b then return true end -- NaN == NaN + return false +end +local mock_controller_map = setmetatable( {}, {__mode='k'} ) +-- All the classes are private +local Action, Argv, Callable, Controller, Mock +Action = {} +-- abstract +Action.generic = class() +function Action.generic:add_close (label) + add_to_set( self, 'closelist', label ) +end +function Action.generic:add_depend (d) + add_to_set( self, 'dependlist', d ) +end +function Action.generic:add_label (label) + add_to_set( self, 'labellist', label ) +end +function Action.generic:assert_satisfied () + assert( self.replay_count <= self.max_replays, "lemock internal error" ) + if not ( +self.min_replays <= self.replay_count + ) then + error( sfmt( "Wrong replay count %d (expected %d..%d) for %s" + , self.replay_count + , self.min_replays, self.max_replays + , self:tostring() + ) + , 0 + ) + end +end +function Action.generic:blocks () + if self:is_satisfied() then + return function () end + end + return elements_of_set( self, 'labellist' ) +end +function Action.generic:closes () + return elements_of_set( self, 'closelist' ) +end +function Action.generic:depends () + return elements_of_set( self, 'dependlist' ) +end +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 +function Action.generic:is_expected () + return self.replay_count < self.max_replays + and not self.is_blocked + and not self.is_closed +end +function Action.generic:is_satisfied () + return +self.min_replays <= self.replay_count +end +function Action.generic:match (key) + if getmetatable(self) ~= getmetatable(key) then return false end + if self.mock ~= key.mock then return false end + return self:is_expected() +end +function Action.generic:new (mock) + local a = object( self ) + a.mock = mock + a.replay_count = 0 + a.min_replays = 1 + a.max_replays = 1 + return a +end +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 +Action.generic_call = class( Action.generic ) +Action.generic_call.can_return = true +function Action.generic_call:get_returnvalue () + if self.has_returnvalue then + return self.returnvalue:unpack() + end +end +function Action.generic_call:set_returnvalue (...) + self.returnvalue = Argv:new(...) + self.has_returnvalue = true +end +function Action.generic_call:match (q) + if not Action.generic.match( self, q ) then return false end + if not self.argv:equal( q.argv ) then return false end + return true +end +function Action.generic_call:new (m, ...) + local a = Action.generic.new( self, m ) + a.argv = Argv:new(...) + return a +end +-- concrete +Action.call = class( Action.generic_call ) +function Action.call:match (q) + if not Action.generic_call.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true +end +function Action.call:new (m, key, ...) + local a = Action.generic_call.new( self, m, ... ) + a.key = key + return a +end +function Action.call:tostring () + if self.has_returnvalue then + return sfmt( "call %s(%s) => %s" + , tostring(self.key) + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "call %s(%s)" + , tostring(self.key) + , self.argv:tostring() + ) + end +end +Action.index = class( Action.generic ) +Action.index.can_return = true +function Action.index:get_returnvalue () + return self.returnvalue +end +function Action.index:set_returnvalue (v) + self.returnvalue = v + self.has_returnvalue = true +end +function Action.index:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true +end +function Action.index:new (m, key) + local a = Action.generic.new( self, m ) + a.key = key + return a +end +function Action.index:tostring () + local key = 'index '..tostring( self.key ) + if self.has_returnvalue then + return sfmt( "index %s => %s" + , tostring( self.key ) + , qtostring( self.returnvalue ) + ) + elseif self.is_callable then + return sfmt( "index %s()" + , tostring( self.key ) + ) + else + return sfmt( "index %s" + , tostring( self.key ) + ) + end +end +Action.newindex = class( Action.generic ) +function Action.newindex:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + if not value_equal( self.val, q.val ) + and self.val ~= Argv.ANYARG + and q.val ~= Argv.ANYARG then return false end + return true +end +function Action.newindex:new (m, key, val) + local a = Action.generic.new( self, m ) + a.key = key + a.val = val + return a +end +function Action.newindex:tostring () + return sfmt( "newindex %s = %s" + , tostring(self.key) + , qtostring(self.val) + ) +end +Action.selfcall = class( Action.generic_call ) +function Action.selfcall:match (q) + return Action.generic_call.match( self, q ) +end +function Action.selfcall:new (m, ...) + local a = Action.generic_call.new( self, m, ... ) + return a +end +function Action.selfcall:tostring () + if self.has_returnvalue then + return sfmt( "selfcall (%s) => %s" + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "selfcall (%s)" + , self.argv:tostring() + ) + end +end +Argv = class() +Argv.ANYARGS = newproxy() local ANYARGS = Argv.ANYARGS +Argv.ANYARG = newproxy() local ANYARG = Argv.ANYARG +function Argv:equal (other) + local a1, n1 = self.v, self.len + local a2, n2 = other.v, other.len + if n1-1 <= n2 and a1[n1] == ANYARGS then + n1 = n1-1 + n2 = n1 + elseif n2-1 <= n1 and a2[n2] == ANYARGS then + n2 = n2-1 + n1 = n2 + end + if n1 ~= n2 then + return false + end + for i = 1, n1 do + local v1, v2 = a1[i], a2[i] + if not value_equal(v1,v2) and v1 ~= ANYARG and v2 ~= ANYARG then + return false + end + end + return true +end +function Argv:new (...) + local av = object( self ) + av.v = {...} + av.len = select('#',...) + for i = 1, av.len - 1 do + if av.v[i] == Argv.ANYARGS then + error( "ANYARGS not at end.", 0 ) + end + end + return av +end +function Argv:tostring () + local res = {} + local function w (v) + res[#res+1] = qtostring( v ) + end + local av, ac = self.v, self.len + for i = 1, ac do + if av[i] == Argv.ANYARG then + res[#res+1] = 'ANYARG' + elseif av[i] == Argv.ANYARGS then + res[#res+1] = 'ANYARGS' + else + w( av[i] ) + end + if i < ac then + res[#res+1] = ',' -- can not use qtostring in w() + end + end + return table.concat( res ) +end +function Argv:unpack () + return unpack( self.v, 1, self.len ) +end +Callable = {} +Callable.generic = class() +Callable.record = class( Callable.generic ) +Callable.replay = class( Callable.generic ) +function Callable.generic:new ( index_action ) + local f = object( self ) + f.action = index_action + return f +end +function Callable.record:__call (...) + local index_action = self.action + local m = index_action.mock + local mc = mock_controller_map[m] + assert( mc.is_recording, "client uses cached callable from recording" ) + mc:make_callable( index_action ) + mc:add_action( Action.call:new( m, index_action.key, ... )) +end +function Callable.replay:__call (...) + local index_action = self.action + local m = index_action.mock + local mc = mock_controller_map[m] + local call_action = mc:lookup( Action.call:new( m, index_action.key, ... )) + mc:replay_action( call_action ) + if call_action.throws_error then + error( call_action.errorvalue, 2 ) + end + return call_action:get_returnvalue() +end +Controller = class() +-- Exported methods +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 +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 +function Controller:error (value) + if not self.is_recording then + error( "Error called during replay.", 2 ) + end + local action = self:get_last_action() + if action.has_returnvalue or action.throws_error then + error( "Returns and/or Error called twice for same action.", 2 ) + end + action.throws_error = true + action.errorvalue = value + return self -- for chaining +end +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 +function Controller:mock () + if not self.is_recording then + error( "New mock during replay.", 2 ) + end + local m = object( Mock.record ) + mock_controller_map[m] = self + return m +end +function Controller:new () + local mc = object( self ) + mc.actionlist = {} + mc.is_recording = true + return mc +end +function Controller:replay () + if not self.is_recording then + error( "Replay called twice.", 2 ) + end + self.is_recording = false + for m, mc in pairs( mock_controller_map ) do + if mc == self then + setmetatable( m, Mock.replay ) + end + end + self:update_dependencies() + self:assert_no_dependency_cycles() +end +function Controller:returns (...) + if not self.is_recording then + error( "Returns called during replay.", 2 ) + end + local action = self:get_last_action() + assert( not action.is_callable, "lemock internal error" ) + if not action.can_return then + error( "Previous action can not return anything.", 2 ) + end + if action.has_returnvalue or action.throws_error then + error( "Returns and/or Error called twice for same action.", 2 ) + end + action:set_returnvalue(...) + return self -- for chaining +end +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 +function Controller:verify () + if self.is_recording then + error( "Verify called during record.", 2 ) + end + for a in self:actions() do + a:assert_satisfied() + end +end +-- Protected methods +function Controller:actions (q) + local l = self.actionlist + local i = 0 + return function () + i = i + 1 + return l[i] + end +end +function Controller:add_action (a) + assert( a ~= nil, "lemock internal error" ) -- breaks array property + table.insert( self.actionlist, a ) +end +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 +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 +function Controller:get_last_action () + local l = self.actionlist + if #l == 0 then + error( "No action is recorded yet.", 0 ) + end + return l[#l] +end +function Controller:lookup (actual) + for action in self:actions() do + if action:match( actual ) then + return action + end + end +local expected = {} +for _, a in ipairs( self.actionlist ) do + if a:is_expected() and not a.is_callable then + expected[#expected+1] = a:tostring() + end +end +table.sort( expected ) +if #expected == 0 then + expected[1] = "(Nothing)" +end + error( sfmt( "Unexpected action %s, expected:\n%s\n" + , actual:tostring() + , table.concat(expected,'\n') + ) + , 0 + ) +end +function Controller:make_callable (action) + if action.has_returnvalue then + error( "Can not call "..action.key..". It has a returnvalue.", 0 ) + end + action.is_callable = true + action.min_replays = 0 + action.max_replays = math.huge +end +function Controller:new () + local mc = object( self ) + mc.actionlist = {} + mc.is_recording = true + return mc +end +function Controller:replay_action ( action ) + assert( action:is_expected(), "lemock internal error" ) + assert( action.replay_count < action.max_replays, "lemock internal error" ) + local was_satisfied = action:is_satisfied() + action.replay_count = action.replay_count + 1 + if not was_satisfied and action.labellist and action:is_satisfied() then + self:update_dependencies() + end + if action.closelist then + self:close_actions( action:closes() ) + end +end +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 +Mock = { record={}, replay={} } -- no self-referencing __index! +function Mock.record:__index (key) + local mc = mock_controller_map[self] + local action = Action.index:new( self, key ) + mc:add_action( action ) + return Callable.record:new( action ) +end +function Mock.record:__newindex (key, val) + local mc = mock_controller_map[self] + mc:add_action( Action.newindex:new( self, key, val )) +end +function Mock.record:__call (...) + local mc = mock_controller_map[self] + mc:add_action( Action.selfcall:new( self, ... )) +end +function Mock.replay:__index (key) + local mc = mock_controller_map[self] + local index_action = mc:lookup( Action.index:new( self, key )) + mc:replay_action( index_action ) + if index_action.throws_error then + error( index_action.errorvalue, 2 ) + end + if index_action.is_callable then + return Callable.replay:new( index_action ) + else + return index_action:get_returnvalue() + end +end +function Mock.replay:__newindex (key, val) + local mc = mock_controller_map[self] + local newindex_action = mc:lookup( Action.newindex:new( self, key, val )) + mc:replay_action( newindex_action ) + if newindex_action.throws_error then + error( newindex_action.errorvalue, 2 ) + end +end +function Mock.replay:__call (...) + local mc = mock_controller_map[self] + local selfcall_action = mc:lookup( Action.selfcall:new( self, ... )) + mc:replay_action( selfcall_action ) + if selfcall_action.throws_error then + error( selfcall_action.errorvalue, 2 ) + end + return selfcall_action:get_returnvalue() +end +function controller () + local exported_methods = { + 'anytimes', + 'atleastonce', + 'close', + 'depend', + 'error', + 'label', + 'mock', + 'new', + 'replay', + 'returns', + 'times', + 'verify', + } + local mc = Controller:new() + local wrapper = {} + for _, method in ipairs( exported_methods ) do + wrapper[ method ] = function (self, ...) + return mc[ method ]( mc, ... ) + end + end + wrapper.ANYARG = Argv.ANYARG + wrapper.ANYARGS = Argv.ANYARGS + return wrapper +end +return _M diff --git a/build/mkfile b/build/mkfile new file mode 100644 index 0000000..16b57ba --- /dev/null +++ b/build/mkfile @@ -0,0 +1,20 @@ +MKSHELL = rc + +all:V: htdocs + +wrappers = `{find www -name '*.t2t'} +htmls = ${wrappers:www/%.t2t=htdocs/%.html} + +htdocs:V: $htmls + +$htmls: www/menubar.html +htdocs/COPYRIGHT.html: ../COPYRIGHT +htdocs/DEVEL.html: ../DEVEL +htdocs/HISTORY.html: ../HISTORY +htdocs/README.html: ../README + +htdocs/%.html: www/%.t2t + txt2tags -t html -i www/$stem.t2t -o $target + +htdocs/userguide.html: www/userguide.t2t userguide.t2t + txt2tags -t html --toc --toc-level 2 -i www/userguide.t2t -o $target diff --git a/build/unit/action.lua b/build/unit/action.lua new file mode 100644 index 0000000..3cc75f0 --- /dev/null +++ b/build/unit/action.lua @@ -0,0 +1,603 @@ +-- ../src/unittestfiles.nw:145 + +-- ../src/misc.nw:7 + ------ THIS FILE IS TANGLED FROM LITERATE SOURCE FILES ------ + -- Copyright (C) 2009 Tommy Pettersson + -- See terms in file COPYRIGHT, or at http://lemock.luaforge.net + +-- ../src/unittestfiles.nw:146 + + require 'lunit' + module( 'unit.action', lunit.testcase, package.seeall ) + + local class, object, qtostring, sfmt + +-- ../src/helperfunctions.nw:12 + function object (class) + return setmetatable( {}, class ) + end + function class (parent) + local c = object(parent) + c.__index = c + return c + end +-- ../src/unittestfiles.nw:152 + +-- ../src/helperfunctions.nw:29 + function value_equal (a, b) + if a == b then return true end + if a ~= a and b ~= b then return true end -- NaN == NaN + return false + end +-- ../src/unittestfiles.nw:153 + +-- ../src/tostring.nw:23 + sfmt = string.format + function qtostring (v) + if type(v) == 'string' then + return sfmt( '%q', v ) + else + return tostring( v ) + end + end +-- ../src/unittestfiles.nw:154 + + local Action, Argv + +-- ../src/class/action.nw:24 + Action = {} + + -- abstract + +-- ../src/class/action.nw:41 + Action.generic = class() + + +-- ../src/restrictions.nw:607 + function Action.generic:add_close (label) + add_to_set( self, 'closelist', label ) + end +-- ../src/class/action.nw:44 + +-- ../src/restrictions.nw:443 + function Action.generic:add_depend (d) + add_to_set( self, 'dependlist', d ) + end + +-- ../src/class/action.nw:45 + +-- ../src/restrictions.nw:207 + function Action.generic:add_label (label) + add_to_set( self, 'labellist', label ) + end + +-- ../src/class/action.nw:46 + +-- ../src/main.nw:338 + function Action.generic:assert_satisfied () + assert( self.replay_count <= self.max_replays, "lemock internal error" ) + if not ( +-- ../src/main.nw:330 + self.min_replays <= self.replay_count + +-- ../src/main.nw:340 + ) then + error( sfmt( "Wrong replay count %d (expected %d..%d) for %s" + , self.replay_count + , self.min_replays, self.max_replays + , self:tostring() + ) + , 0 + ) + end + end +-- ../src/class/action.nw:47 + +-- ../src/restrictions.nw:220 + function Action.generic:blocks () + if self:is_satisfied() then + return function () end + end + return elements_of_set( self, 'labellist' ) + end +-- ../src/class/action.nw:48 + +-- ../src/restrictions.nw:630 + function Action.generic:closes () + return elements_of_set( self, 'closelist' ) + end +-- ../src/class/action.nw:49 + +-- ../src/restrictions.nw:448 + function Action.generic:depends () + return elements_of_set( self, 'dependlist' ) + end +-- ../src/class/action.nw:50 + +-- ../src/restrictions.nw:212 + 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 + +-- ../src/class/action.nw:51 + +-- ../src/main.nw:247 + function Action.generic:is_expected () + return self.replay_count < self.max_replays + and not self.is_blocked + and not self.is_closed + end + +-- ../src/class/action.nw:52 + +-- ../src/main.nw:333 + function Action.generic:is_satisfied () + return +-- ../src/main.nw:330 + self.min_replays <= self.replay_count + +-- ../src/main.nw:335 + end + +-- ../src/class/action.nw:53 + +-- ../src/main.nw:269 + function Action.generic:match (key) + if getmetatable(self) ~= getmetatable(key) then return false end + if self.mock ~= key.mock then return false end + return self:is_expected() + end +-- ../src/class/action.nw:54 + +-- ../src/main.nw:219 + function Action.generic:new (mock) + local a = object( self ) + a.mock = mock + a.replay_count = 0 + a.min_replays = 1 + a.max_replays = 1 + return a + end +-- ../src/class/action.nw:55 + +-- ../src/restrictions.nw:102 + 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 + + +-- ../src/class/action.nw:28 + +-- ../src/class/action.nw:59 + Action.generic_call = class( Action.generic ) + + Action.generic_call.can_return = true + +-- ../src/action/generic_call.nw:76 + function Action.generic_call:get_returnvalue () + if self.has_returnvalue then + return self.returnvalue:unpack() + end + end +-- ../src/class/action.nw:63 + +-- ../src/action/generic_call.nw:56 + function Action.generic_call:set_returnvalue (...) + self.returnvalue = Argv:new(...) + self.has_returnvalue = true + end +-- ../src/class/action.nw:64 + + +-- ../src/action/generic_call.nw:45 + function Action.generic_call:match (q) + if not Action.generic.match( self, q ) then return false end + if not self.argv:equal( q.argv ) then return false end + return true + end +-- ../src/class/action.nw:66 + +-- ../src/action/generic_call.nw:32 + function Action.generic_call:new (m, ...) + local a = Action.generic.new( self, m ) + a.argv = Argv:new(...) + return a + end +-- ../src/class/action.nw:29 + + -- concrete + +-- ../src/class/action.nw:93 + Action.call = class( Action.generic_call ) + + +-- ../src/action/call.nw:118 + function Action.call:match (q) + if not Action.generic_call.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true + end +-- ../src/class/action.nw:96 + +-- ../src/action/call.nw:82 + function Action.call:new (m, key, ...) + local a = Action.generic_call.new( self, m, ... ) + a.key = key + return a + end +-- ../src/class/action.nw:97 + +-- ../src/tostring.nw:101 + function Action.call:tostring () + if self.has_returnvalue then + return sfmt( "call %s(%s) => %s" + , tostring(self.key) + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "call %s(%s)" + , tostring(self.key) + , self.argv:tostring() + ) + end + end + + +-- ../src/class/action.nw:32 + +-- ../src/class/action.nw:81 + Action.index = class( Action.generic ) + + Action.index.can_return = true + +-- ../src/action/index.nw:134 + function Action.index:get_returnvalue () + return self.returnvalue + end +-- ../src/class/action.nw:85 + +-- ../src/action/index.nw:85 + function Action.index:set_returnvalue (v) + self.returnvalue = v + self.has_returnvalue = true + end +-- ../src/class/action.nw:86 + + +-- ../src/action/index.nw:123 + function Action.index:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true + end +-- ../src/class/action.nw:88 + +-- ../src/action/index.nw:67 + function Action.index:new (m, key) + local a = Action.generic.new( self, m ) + a.key = key + return a + end +-- ../src/class/action.nw:89 + +-- ../src/tostring.nw:70 + function Action.index:tostring () + local key = 'index '..tostring( self.key ) + if self.has_returnvalue then + return sfmt( "index %s => %s" + , tostring( self.key ) + , qtostring( self.returnvalue ) + ) + elseif self.is_callable then + return sfmt( "index %s()" + , tostring( self.key ) + ) + else + return sfmt( "index %s" + , tostring( self.key ) + ) + end + end + + +-- ../src/class/action.nw:33 + +-- ../src/class/action.nw:73 + Action.newindex = class( Action.generic ) + + +-- ../src/action/newindex.nw:102 + function Action.newindex:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + if not value_equal( self.val, q.val ) + and self.val ~= Argv.ANYARG + and q.val ~= Argv.ANYARG then return false end + return true + end +-- ../src/class/action.nw:76 + +-- ../src/action/newindex.nw:54 + function Action.newindex:new (m, key, val) + local a = Action.generic.new( self, m ) + a.key = key + a.val = val + return a + end +-- ../src/class/action.nw:77 + +-- ../src/tostring.nw:45 + function Action.newindex:tostring () + return sfmt( "newindex %s = %s" + , tostring(self.key) + , qtostring(self.val) + ) + end + + +-- ../src/class/action.nw:34 + +-- ../src/class/action.nw:101 + Action.selfcall = class( Action.generic_call ) + + +-- ../src/action/selfcall.nw:93 + function Action.selfcall:match (q) + return Action.generic_call.match( self, q ) + end +-- ../src/class/action.nw:104 + +-- ../src/action/selfcall.nw:61 + function Action.selfcall:new (m, ...) + local a = Action.generic_call.new( self, m, ... ) + return a + end +-- ../src/class/action.nw:105 + +-- ../src/tostring.nw:129 + function Action.selfcall:tostring () + if self.has_returnvalue then + return sfmt( "selfcall (%s) => %s" + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "selfcall (%s)" + , self.argv:tostring() + ) + end + end +-- ../src/unittestfiles.nw:157 + +-- ../src/class/argv.nw:6 + Argv = class() + + +-- ../src/argv.nw:119 + Argv.ANYARGS = newproxy() local ANYARGS = Argv.ANYARGS + Argv.ANYARG = newproxy() local ANYARG = Argv.ANYARG + function Argv:equal (other) + local a1, n1 = self.v, self.len + local a2, n2 = other.v, other.len + if n1-1 <= n2 and a1[n1] == ANYARGS then + n1 = n1-1 + n2 = n1 + elseif n2-1 <= n1 and a2[n2] == ANYARGS then + n2 = n2-1 + n1 = n2 + end + if n1 ~= n2 then + return false + end + for i = 1, n1 do + local v1, v2 = a1[i], a2[i] + if not value_equal(v1,v2) and v1 ~= ANYARG and v2 ~= ANYARG then + return false + end + end + return true + end +-- ../src/class/argv.nw:9 + +-- ../src/argv.nw:46 + function Argv:new (...) + local av = object( self ) + av.v = {...} + av.len = select('#',...) + for i = 1, av.len - 1 do + if av.v[i] == Argv.ANYARGS then + error( "ANYARGS not at end.", 0 ) + end + end + return av + end +-- ../src/class/argv.nw:10 + +-- ../src/tostring.nw:163 + function Argv:tostring () + local res = {} + local function w (v) + res[#res+1] = qtostring( v ) + end + local av, ac = self.v, self.len + for i = 1, ac do + if av[i] == Argv.ANYARG then + res[#res+1] = 'ANYARG' + elseif av[i] == Argv.ANYARGS then + res[#res+1] = 'ANYARGS' + else + w( av[i] ) + end + if i < ac then + res[#res+1] = ',' -- can not use qtostring in w() + end + end + return table.concat( res ) + end +-- ../src/class/argv.nw:11 + +-- ../src/argv.nw:156 + function Argv:unpack () + return unpack( self.v, 1, self.len ) + end +-- ../src/unittestfiles.nw:158 + + +-- ../src/action/call.nw:106 + function call_match_test () + local m = {} + local a = Action.call:new( m, 'foo', 4, 'bb' ) + assert_true( a:match( Action.call:new( m, 'foo', 4, 'bb' ))) + assert_false( a:match( Action.call:new( {}, 'foo', 4, 'bb' ))) + assert_false( a:match( Action.call:new( m, 'bar', 4, 'bb' ))) + assert_false( a:match( Action.call:new( m, 'foo', 1, 'bb' ))) + assert_false( a:match( Action.call:new( m, 'foo', 4, 'b' ))) + assert_false( a:match( Action.call:new( m, 'foo', 4, 'bb', 'cc' ))) + end + +-- ../src/unittestfiles.nw:160 + +-- ../src/tostring.nw:93 + function call_tostring_test () + local a = Action.call:new( {}, 'foo', 1, '"', 3 ) + assert_equal( 'call foo(1,"\\"",3)', a:tostring() ) + a:set_returnvalue( 'false', false ) + assert_equal( 'call foo(1,"\\"",3) => "false",false', a:tostring() ) + end + +-- ../src/unittestfiles.nw:161 + +-- ../src/action/generic_call.nw:66 + function generic_call_set_and_get_returnvalue_test () + local a = Action.generic_call:new() + assert_equal( 0, select('#', a:get_returnvalue() )) + a:set_returnvalue( nil, false ) + local r1, r2 = a:get_returnvalue() + assert_equal( nil, r1 ) + assert_equal( false, r2 ) + end + +-- ../src/unittestfiles.nw:162 + +-- ../src/action/index.nw:114 + function index_match_test () + local m = {} + local a = Action.index:new( m, -1 ) + assert_true( a:match( Action.index:new( m, -1 ))) + assert_false( a:match( Action.index:new( {}, -1 ))) + assert_false( a:match( Action.index:new( m, 'a' ))) + end + +-- ../src/unittestfiles.nw:163 + +-- ../src/action/index.nw:59 + function create_index_action_test () + local m = {} + local a = Action.index:new( m, 'foo' ) + assert_equal( m, a.mock ) + assert_equal( 'foo', a.key ) + end + +-- ../src/unittestfiles.nw:164 + +-- ../src/action/index.nw:78 + function index_returnvalue_test () + local a = Action.index:new( {}, -3 ) + a:set_returnvalue( 'foo' ) + assert_equal( 'foo', a:get_returnvalue() ) + end + +-- ../src/unittestfiles.nw:165 + +-- ../src/tostring.nw:57 + function index_tostring_test () + local a = Action.index:new( {}, true ) + assert_equal( 'index true', a:tostring() ) + a:set_returnvalue('"false"') + assert_equal( 'index true => "\\"false\\""', a:tostring() ) + end + function callable_index_tostring_test () + local a = Action.index:new( {}, 'f' ) + a.is_callable = true + assert_equal( 'index f()', a:tostring() ) + end + +-- ../src/unittestfiles.nw:166 + +-- ../src/action/newindex.nw:76 + function newindex_match_test () + local m = {} + local a = Action.newindex:new( m, 'foo', 17 ) + assert_true( a:match( Action.newindex:new( m, 'foo', 17 ))) + assert_false( a:match( Action.newindex:new( {}, 'foo', 17 ))) + assert_false( a:match( Action.newindex:new( m, 'fo', 17 ))) + assert_false( a:match( Action.newindex:new( m, 'foo', 7 ))) + end + function newindex_anyarg_test () + local m = {} + local a = Action.newindex:new( m, 'foo', Argv.ANYARG ) + local b = Action.newindex:new( m, 'foo', 33 ) + local c = Action.newindex:new( m, 'foo', nil ) + assert_true( a:match(b) ) + assert_true( b:match(a) ) + assert_true( a:match(c) ) + assert_true( c:match(a) ) + end + function newindex_NaN_test () + local m = {} + local nan = 0/0 + local a = Action.newindex:new( m, m, nan ) + assert_true( a:match( Action.newindex:new( m, m, nan ))) + end + +-- ../src/unittestfiles.nw:167 + +-- ../src/tostring.nw:37 + function newindex_tostring_test () + local a = Action.newindex:new( {}, 'key', 7 ) + assert_equal( 'newindex key = 7', a:tostring() ) + a = Action.newindex:new( {}, true, '7' ) + assert_equal( 'newindex true = "7"', a:tostring() ) + end + +-- ../src/unittestfiles.nw:168 + +-- ../src/action/selfcall.nw:82 + function selfcall_match_test () + local m = {} + local a = Action.selfcall:new( m, 5, nil, false ) + assert_true( a:match( Action.selfcall:new( m, 5, nil, false ))) + assert_false( a:match( Action.selfcall:new( {}, 5, nil, false ))) + assert_false( a:match( Action.selfcall:new( m, nil, nil, false ))) + assert_false( a:match( Action.selfcall:new( m, 5, false, false ))) + assert_false( a:match( Action.selfcall:new( m, 5, nil, nil ))) + end + +-- ../src/unittestfiles.nw:169 + +-- ../src/tostring.nw:121 + function selfcall_tostring_test () + local a = Action.selfcall:new( {}, 1, '"', nil ) + assert_equal( 'selfcall (1,"\\"",nil)', a:tostring() ) + a:set_returnvalue( 'false', false ) + assert_equal( 'selfcall (1,"\\"",nil) => "false",false', a:tostring() ) + end + diff --git a/build/unit/action_generic.lua b/build/unit/action_generic.lua new file mode 100644 index 0000000..7b91c3b --- /dev/null +++ b/build/unit/action_generic.lua @@ -0,0 +1,598 @@ +-- ../src/unittestfiles.nw:108 + +-- ../src/misc.nw:7 + ------ THIS FILE IS TANGLED FROM LITERATE SOURCE FILES ------ + -- Copyright (C) 2009 Tommy Pettersson + -- See terms in file COPYRIGHT, or at http://lemock.luaforge.net + +-- ../src/unittestfiles.nw:109 + + require 'lunit' + module( 'unit.action_generic', lunit.testcase, package.seeall ) + + local class, object, qtostring, sfmt, add_to_set, elements_of_set + +-- ../src/helperfunctions.nw:12 + function object (class) + return setmetatable( {}, class ) + end + function class (parent) + local c = object(parent) + c.__index = c + return c + end +-- ../src/unittestfiles.nw:115 + +-- ../src/tostring.nw:23 + sfmt = string.format + function qtostring (v) + if type(v) == 'string' then + return sfmt( '%q', v ) + else + return tostring( v ) + end + end +-- ../src/unittestfiles.nw:116 + +-- ../src/helperfunctions.nw:47 + function add_to_set (o, setname, element) + if not o[setname] then + o[setname] = {} + end + local l = o[setname] + + for i = 1, #l do + if l[i] == element then return end + end + l[#l+1] = element + end + function elements_of_set (o, setname) + local l = o[setname] + local i = l and #l+1 or 0 + return function () + i = i - 1 + if i > 0 then return l[i] end + end + end +-- ../src/unittestfiles.nw:117 + + local Action, Argv + +-- ../src/class/action.nw:24 + Action = {} + + -- abstract + +-- ../src/class/action.nw:41 + Action.generic = class() + + +-- ../src/restrictions.nw:607 + function Action.generic:add_close (label) + add_to_set( self, 'closelist', label ) + end +-- ../src/class/action.nw:44 + +-- ../src/restrictions.nw:443 + function Action.generic:add_depend (d) + add_to_set( self, 'dependlist', d ) + end + +-- ../src/class/action.nw:45 + +-- ../src/restrictions.nw:207 + function Action.generic:add_label (label) + add_to_set( self, 'labellist', label ) + end + +-- ../src/class/action.nw:46 + +-- ../src/main.nw:338 + function Action.generic:assert_satisfied () + assert( self.replay_count <= self.max_replays, "lemock internal error" ) + if not ( +-- ../src/main.nw:330 + self.min_replays <= self.replay_count + +-- ../src/main.nw:340 + ) then + error( sfmt( "Wrong replay count %d (expected %d..%d) for %s" + , self.replay_count + , self.min_replays, self.max_replays + , self:tostring() + ) + , 0 + ) + end + end +-- ../src/class/action.nw:47 + +-- ../src/restrictions.nw:220 + function Action.generic:blocks () + if self:is_satisfied() then + return function () end + end + return elements_of_set( self, 'labellist' ) + end +-- ../src/class/action.nw:48 + +-- ../src/restrictions.nw:630 + function Action.generic:closes () + return elements_of_set( self, 'closelist' ) + end +-- ../src/class/action.nw:49 + +-- ../src/restrictions.nw:448 + function Action.generic:depends () + return elements_of_set( self, 'dependlist' ) + end +-- ../src/class/action.nw:50 + +-- ../src/restrictions.nw:212 + 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 + +-- ../src/class/action.nw:51 + +-- ../src/main.nw:247 + function Action.generic:is_expected () + return self.replay_count < self.max_replays + and not self.is_blocked + and not self.is_closed + end + +-- ../src/class/action.nw:52 + +-- ../src/main.nw:333 + function Action.generic:is_satisfied () + return +-- ../src/main.nw:330 + self.min_replays <= self.replay_count + +-- ../src/main.nw:335 + end + +-- ../src/class/action.nw:53 + +-- ../src/main.nw:269 + function Action.generic:match (key) + if getmetatable(self) ~= getmetatable(key) then return false end + if self.mock ~= key.mock then return false end + return self:is_expected() + end +-- ../src/class/action.nw:54 + +-- ../src/main.nw:219 + function Action.generic:new (mock) + local a = object( self ) + a.mock = mock + a.replay_count = 0 + a.min_replays = 1 + a.max_replays = 1 + return a + end +-- ../src/class/action.nw:55 + +-- ../src/restrictions.nw:102 + 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 + + +-- ../src/class/action.nw:28 + +-- ../src/class/action.nw:59 + Action.generic_call = class( Action.generic ) + + Action.generic_call.can_return = true + +-- ../src/action/generic_call.nw:76 + function Action.generic_call:get_returnvalue () + if self.has_returnvalue then + return self.returnvalue:unpack() + end + end +-- ../src/class/action.nw:63 + +-- ../src/action/generic_call.nw:56 + function Action.generic_call:set_returnvalue (...) + self.returnvalue = Argv:new(...) + self.has_returnvalue = true + end +-- ../src/class/action.nw:64 + + +-- ../src/action/generic_call.nw:45 + function Action.generic_call:match (q) + if not Action.generic.match( self, q ) then return false end + if not self.argv:equal( q.argv ) then return false end + return true + end +-- ../src/class/action.nw:66 + +-- ../src/action/generic_call.nw:32 + function Action.generic_call:new (m, ...) + local a = Action.generic.new( self, m ) + a.argv = Argv:new(...) + return a + end +-- ../src/class/action.nw:29 + + -- concrete + +-- ../src/class/action.nw:93 + Action.call = class( Action.generic_call ) + + +-- ../src/action/call.nw:118 + function Action.call:match (q) + if not Action.generic_call.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true + end +-- ../src/class/action.nw:96 + +-- ../src/action/call.nw:82 + function Action.call:new (m, key, ...) + local a = Action.generic_call.new( self, m, ... ) + a.key = key + return a + end +-- ../src/class/action.nw:97 + +-- ../src/tostring.nw:101 + function Action.call:tostring () + if self.has_returnvalue then + return sfmt( "call %s(%s) => %s" + , tostring(self.key) + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "call %s(%s)" + , tostring(self.key) + , self.argv:tostring() + ) + end + end + + +-- ../src/class/action.nw:32 + +-- ../src/class/action.nw:81 + Action.index = class( Action.generic ) + + Action.index.can_return = true + +-- ../src/action/index.nw:134 + function Action.index:get_returnvalue () + return self.returnvalue + end +-- ../src/class/action.nw:85 + +-- ../src/action/index.nw:85 + function Action.index:set_returnvalue (v) + self.returnvalue = v + self.has_returnvalue = true + end +-- ../src/class/action.nw:86 + + +-- ../src/action/index.nw:123 + function Action.index:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true + end +-- ../src/class/action.nw:88 + +-- ../src/action/index.nw:67 + function Action.index:new (m, key) + local a = Action.generic.new( self, m ) + a.key = key + return a + end +-- ../src/class/action.nw:89 + +-- ../src/tostring.nw:70 + function Action.index:tostring () + local key = 'index '..tostring( self.key ) + if self.has_returnvalue then + return sfmt( "index %s => %s" + , tostring( self.key ) + , qtostring( self.returnvalue ) + ) + elseif self.is_callable then + return sfmt( "index %s()" + , tostring( self.key ) + ) + else + return sfmt( "index %s" + , tostring( self.key ) + ) + end + end + + +-- ../src/class/action.nw:33 + +-- ../src/class/action.nw:73 + Action.newindex = class( Action.generic ) + + +-- ../src/action/newindex.nw:102 + function Action.newindex:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + if not value_equal( self.val, q.val ) + and self.val ~= Argv.ANYARG + and q.val ~= Argv.ANYARG then return false end + return true + end +-- ../src/class/action.nw:76 + +-- ../src/action/newindex.nw:54 + function Action.newindex:new (m, key, val) + local a = Action.generic.new( self, m ) + a.key = key + a.val = val + return a + end +-- ../src/class/action.nw:77 + +-- ../src/tostring.nw:45 + function Action.newindex:tostring () + return sfmt( "newindex %s = %s" + , tostring(self.key) + , qtostring(self.val) + ) + end + + +-- ../src/class/action.nw:34 + +-- ../src/class/action.nw:101 + Action.selfcall = class( Action.generic_call ) + + +-- ../src/action/selfcall.nw:93 + function Action.selfcall:match (q) + return Action.generic_call.match( self, q ) + end +-- ../src/class/action.nw:104 + +-- ../src/action/selfcall.nw:61 + function Action.selfcall:new (m, ...) + local a = Action.generic_call.new( self, m, ... ) + return a + end +-- ../src/class/action.nw:105 + +-- ../src/tostring.nw:129 + function Action.selfcall:tostring () + if self.has_returnvalue then + return sfmt( "selfcall (%s) => %s" + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "selfcall (%s)" + , self.argv:tostring() + ) + end + end +-- ../src/unittestfiles.nw:120 + +-- ../src/class/argv.nw:6 + Argv = class() + + +-- ../src/argv.nw:119 + Argv.ANYARGS = newproxy() local ANYARGS = Argv.ANYARGS + Argv.ANYARG = newproxy() local ANYARG = Argv.ANYARG + function Argv:equal (other) + local a1, n1 = self.v, self.len + local a2, n2 = other.v, other.len + if n1-1 <= n2 and a1[n1] == ANYARGS then + n1 = n1-1 + n2 = n1 + elseif n2-1 <= n1 and a2[n2] == ANYARGS then + n2 = n2-1 + n1 = n2 + end + if n1 ~= n2 then + return false + end + for i = 1, n1 do + local v1, v2 = a1[i], a2[i] + if not value_equal(v1,v2) and v1 ~= ANYARG and v2 ~= ANYARG then + return false + end + end + return true + end +-- ../src/class/argv.nw:9 + +-- ../src/argv.nw:46 + function Argv:new (...) + local av = object( self ) + av.v = {...} + av.len = select('#',...) + for i = 1, av.len - 1 do + if av.v[i] == Argv.ANYARGS then + error( "ANYARGS not at end.", 0 ) + end + end + return av + end +-- ../src/class/argv.nw:10 + +-- ../src/tostring.nw:163 + function Argv:tostring () + local res = {} + local function w (v) + res[#res+1] = qtostring( v ) + end + local av, ac = self.v, self.len + for i = 1, ac do + if av[i] == Argv.ANYARG then + res[#res+1] = 'ANYARG' + elseif av[i] == Argv.ANYARGS then + res[#res+1] = 'ANYARGS' + else + w( av[i] ) + end + if i < ac then + res[#res+1] = ',' -- can not use qtostring in w() + end + end + return table.concat( res ) + end +-- ../src/class/argv.nw:11 + +-- ../src/argv.nw:156 + function Argv:unpack () + return unpack( self.v, 1, self.len ) + end +-- ../src/unittestfiles.nw:121 + + local A = Action.generic + Action = nil -- only allow generic action + function A:tostring () return "" end + + local a + + function setup () + a = A:new() + end + + +-- ../src/restrictions.nw:422 + 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 + +-- ../src/unittestfiles.nw:133 + +-- ../src/restrictions.nw:178 + 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 + +-- ../src/unittestfiles.nw:134 + +-- ../src/main.nw:242 + function expect_unreplayed_action_test () + assert_true( a:is_expected() ) + end + +-- ../src/unittestfiles.nw:135 + +-- ../src/main.nw:320 + function unreplayed_action_is_not_satisfied_test () + assert_false( a:is_satisfied() ) + end + function assert_satisfied_unreplayed_action_fails_test () + local ok, err = pcall( function() a:assert_satisfied() end ) + assert_false( ok, "unreplayed action was satisfied" ) + assert_match( "Wrong replay count 0", err ) + end + +-- ../src/unittestfiles.nw:136 + +-- ../src/main.nw:254 + function match_unreplayed_test () + assert_true( a:match( a )) + end + function match_rejects_replayed_action_test () + a.replay_count = 1 + assert_false( a:match( a )) + end + function match_rejects_wrong_action_type_test () + -- Fake different type + local B = class( A ) + local b = B:new() + assert_false( a:match( b )) + end + +-- ../src/unittestfiles.nw:137 + +-- ../src/main.nw:212 + function new_action_has_right_default_values_test () + assert_equal( 0, a.replay_count ) + assert_equal( 1, a.min_replays ) + assert_equal( 1, a.max_replays ) + end + +-- ../src/unittestfiles.nw:138 + +-- ../src/restrictions.nw:90 + 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 + diff --git a/build/unit/argv.lua b/build/unit/argv.lua new file mode 100644 index 0000000..ddecbcf --- /dev/null +++ b/build/unit/argv.lua @@ -0,0 +1,213 @@ +-- ../src/unittestfiles.nw:85 + +-- ../src/misc.nw:7 + ------ THIS FILE IS TANGLED FROM LITERATE SOURCE FILES ------ + -- Copyright (C) 2009 Tommy Pettersson + -- See terms in file COPYRIGHT, or at http://lemock.luaforge.net + +-- ../src/unittestfiles.nw:86 + + require 'lunit' + module( 'unit.argv', lunit.testcase, package.seeall ) + + local class, object, value_equal, sfmt, qtostring + +-- ../src/helperfunctions.nw:12 + function object (class) + return setmetatable( {}, class ) + end + function class (parent) + local c = object(parent) + c.__index = c + return c + end +-- ../src/unittestfiles.nw:92 + +-- ../src/helperfunctions.nw:29 + function value_equal (a, b) + if a == b then return true end + if a ~= a and b ~= b then return true end -- NaN == NaN + return false + end +-- ../src/unittestfiles.nw:93 + +-- ../src/tostring.nw:23 + sfmt = string.format + function qtostring (v) + if type(v) == 'string' then + return sfmt( '%q', v ) + else + return tostring( v ) + end + end +-- ../src/unittestfiles.nw:94 + + local Argv + +-- ../src/class/argv.nw:6 + Argv = class() + + +-- ../src/argv.nw:119 + Argv.ANYARGS = newproxy() local ANYARGS = Argv.ANYARGS + Argv.ANYARG = newproxy() local ANYARG = Argv.ANYARG + function Argv:equal (other) + local a1, n1 = self.v, self.len + local a2, n2 = other.v, other.len + if n1-1 <= n2 and a1[n1] == ANYARGS then + n1 = n1-1 + n2 = n1 + elseif n2-1 <= n1 and a2[n2] == ANYARGS then + n2 = n2-1 + n1 = n2 + end + if n1 ~= n2 then + return false + end + for i = 1, n1 do + local v1, v2 = a1[i], a2[i] + if not value_equal(v1,v2) and v1 ~= ANYARG and v2 ~= ANYARG then + return false + end + end + return true + end +-- ../src/class/argv.nw:9 + +-- ../src/argv.nw:46 + function Argv:new (...) + local av = object( self ) + av.v = {...} + av.len = select('#',...) + for i = 1, av.len - 1 do + if av.v[i] == Argv.ANYARGS then + error( "ANYARGS not at end.", 0 ) + end + end + return av + end +-- ../src/class/argv.nw:10 + +-- ../src/tostring.nw:163 + function Argv:tostring () + local res = {} + local function w (v) + res[#res+1] = qtostring( v ) + end + local av, ac = self.v, self.len + for i = 1, ac do + if av[i] == Argv.ANYARG then + res[#res+1] = 'ANYARG' + elseif av[i] == Argv.ANYARGS then + res[#res+1] = 'ANYARGS' + else + w( av[i] ) + end + if i < ac then + res[#res+1] = ',' -- can not use qtostring in w() + end + end + return table.concat( res ) + end +-- ../src/class/argv.nw:11 + +-- ../src/argv.nw:156 + function Argv:unpack () + return unpack( self.v, 1, self.len ) + end +-- ../src/unittestfiles.nw:97 + + +-- ../src/argv.nw:63 + local l = {} + local function p (...) l[#l+1] = { n=select('#',...), ... } end + p() p(nil) p(nil,nil) p(false) p({}) p(false,nil,{},nil) p(nil,p) + p(true) p(0.1,'','a') p(1/0,nil,0/0) p(0/0) p(0/0, true) p(0/0, false) + function equal_test () + local a1, a2, f, op + for i = 1, #l do + ai = Argv:new( unpack( l[i], 1, l[i].n )) + for j = 1, #l do + aj = Argv:new( unpack( l[j], 1, l[j].n )) + if i == j then + f, op = assert_true, ') ~= (' + else + f, op = assert_false, ') == (' + end + f( ai:equal(aj), '('..ai:tostring()..op..aj:tostring()..')' ) + end + end + end + function equal_anyargs_test () + local a, b = {}, {} + a[1] = Argv:new( Argv.ANYARGS ) + a[2] = Argv:new( 6, Argv.ANYARGS ) + a[3] = Argv:new( 6, 5, Argv.ANYARGS ) + for i = 1, #l do + b[1] = Argv:new( unpack( l[i], 1, l[i].n )) + b[2] = Argv:new( 6, unpack( l[i], 1, l[i].n )) + b[3] = Argv:new( 6, 5, unpack( l[i], 1, l[i].n )) + for j = 1, 3 do + local astr = '('..a[j]:tostring()..')' + local bstr = '('..b[j]:tostring()..')' + assert_true( a[j]:equal(b[j]), astr..' ~= '..bstr ) + assert_true( b[j]:equal(a[j]), bstr..' ~= '..astr ) + end + end + end + function equal_anyarg_test () + local l = { 1, 2, 3, 4, 5, 6, 7, 8, 9 } + local a1 = Argv:new( unpack(l) ) + for i = 1, 9 do + l[i] = Argv.ANYARG + local a2 = Argv:new( unpack(l) ) + assert_true( a1:equal(a2) ) + assert_true( a2:equal(a1) ) + l[i] = i + end + end +-- ../src/unittestfiles.nw:99 + +-- ../src/argv.nw:27 + function new_test () + Argv:new( Argv.ANYARGS ) + Argv:new( 1, Argv.ANYARGS ) + Argv:new( 1, 2, Argv.ANYARGS ) + end + function new_anyargs_with_extra_arguments_fails_test () + local l = {} + l['ANYARGS,1'] = { Argv.ANYARGS, 1 } + l['ANYARGS,ANYARGS' ] = { Argv.ANYARGS, Argv.ANYARGS } + l['1,ANYARGS,1'] = { 1, Argv.ANYARGS, 1 } + l['1,ANYARGS,ANYARGS'] = { 1, Argv.ANYARGS, Argv.ANYARGS } + for msg, args in pairs( l ) do + local ok, err = pcall( function() Argv:new( unpack(args) ) end ) + assert_false( ok, "Bad ANYARGS accepted for "..msg ) + assert_match( "ANYARGS not at end", err ) + end + end + +-- ../src/unittestfiles.nw:100 + +-- ../src/tostring.nw:151 + function tostring_test () + assert_equal( '', Argv:new() :tostring() ) + assert_equal( '""', Argv:new('') :tostring() ) + assert_equal( 'nil,nil', Argv:new(nil,nil) :tostring() ) + assert_equal( '"false",false', Argv:new('false',false) :tostring() ) + assert_equal( '1,2,3', Argv:new(1,2,3) :tostring() ) + assert_equal( '1,ANYARG,3', Argv:new(1,Argv.ANYARG,3):tostring() ) + assert_equal( 'ANYARGS', Argv:new(Argv.ANYARGS) :tostring() ) + assert_equal( '7,0,ANYARGS', Argv:new(7,0,Argv.ANYARGS):tostring() ) + end + +-- ../src/unittestfiles.nw:101 + +-- ../src/argv.nw:148 + function unpack_test () + local a, b, c = Argv:new( false, nil, 7 ):unpack() + assert_equal( false, a ) + assert_equal( nil, b ) + assert_equal( 7, c ) + end + diff --git a/build/unit/controller.lua b/build/unit/controller.lua new file mode 100644 index 0000000..5bb22e6 --- /dev/null +++ b/build/unit/controller.lua @@ -0,0 +1,860 @@ +-- ../src/unittestfiles.nw:46 + +-- ../src/misc.nw:7 + ------ THIS FILE IS TANGLED FROM LITERATE SOURCE FILES ------ + -- Copyright (C) 2009 Tommy Pettersson + -- See terms in file COPYRIGHT, or at http://lemock.luaforge.net + +-- ../src/unittestfiles.nw:47 + + require 'lunit' + module( 'unit.controller', lunit.testcase, package.seeall ) + + local class, object, qtostring, sfmt, add_to_set, elements_of_set + +-- ../src/helperfunctions.nw:12 + function object (class) + return setmetatable( {}, class ) + end + function class (parent) + local c = object(parent) + c.__index = c + return c + end +-- ../src/unittestfiles.nw:53 + +-- ../src/tostring.nw:23 + sfmt = string.format + function qtostring (v) + if type(v) == 'string' then + return sfmt( '%q', v ) + else + return tostring( v ) + end + end +-- ../src/unittestfiles.nw:54 + +-- ../src/helperfunctions.nw:47 + function add_to_set (o, setname, element) + if not o[setname] then + o[setname] = {} + end + local l = o[setname] + + for i = 1, #l do + if l[i] == element then return end + end + l[#l+1] = element + end + function elements_of_set (o, setname) + local l = o[setname] + local i = l and #l+1 or 0 + return function () + i = i - 1 + if i > 0 then return l[i] end + end + end +-- ../src/unittestfiles.nw:55 + + +-- ../src/main.nw:373 + local mock_controller_map = setmetatable( {}, {__mode='k'} ) +-- ../src/unittestfiles.nw:57 + + local Controller, Action + +-- ../src/class/controller.nw:6 + Controller = class() + + -- Exported methods + +-- ../src/restrictions.nw:595 + 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 + +-- ../src/class/controller.nw:10 + +-- ../src/restrictions.nw:410 + 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 + +-- ../src/class/controller.nw:11 + +-- ../src/main.nw:617 + function Controller:error (value) + if not self.is_recording then + error( "Error called during replay.", 2 ) + end + local action = self:get_last_action() + if action.has_returnvalue or action.throws_error then + error( "Returns and/or Error called twice for same action.", 2 ) + end + action.throws_error = true + action.errorvalue = value + return self -- for chaining + end +-- ../src/class/controller.nw:12 + +-- ../src/restrictions.nw:158 + 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 +-- ../src/class/controller.nw:13 + +-- ../src/main.nw:462 + function Controller:mock () + if not self.is_recording then + error( "New mock during replay.", 2 ) + end + local m = object( Mock.record ) + mock_controller_map[m] = self + return m + end +-- ../src/class/controller.nw:14 + +-- ../src/main.nw:421 + function Controller:new () + local mc = object( self ) + mc.actionlist = {} + mc.is_recording = true + return mc + end +-- ../src/class/controller.nw:15 + +-- ../src/main.nw:671 + function Controller:replay () + if not self.is_recording then + error( "Replay called twice.", 2 ) + end + self.is_recording = false + for m, mc in pairs( mock_controller_map ) do + if mc == self then + setmetatable( m, Mock.replay ) + end + end + self:update_dependencies() + self:assert_no_dependency_cycles() + end +-- ../src/class/controller.nw:16 + +-- ../src/main.nw:571 + function Controller:returns (...) + if not self.is_recording then + error( "Returns called during replay.", 2 ) + end + local action = self:get_last_action() + assert( not action.is_callable, "lemock internal error" ) + if not action.can_return then + error( "Previous action can not return anything.", 2 ) + end + if action.has_returnvalue or action.throws_error then + error( "Returns and/or Error called twice for same action.", 2 ) + end + action:set_returnvalue(...) + return self -- for chaining + end +-- ../src/class/controller.nw:17 + +-- ../src/restrictions.nw:74 + 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 +-- ../src/class/controller.nw:18 + +-- ../src/main.nw:754 + function Controller:verify () + if self.is_recording then + error( "Verify called during record.", 2 ) + end + for a in self:actions() do + a:assert_satisfied() + end + end +-- ../src/class/controller.nw:19 + + -- Protected methods + +-- ../src/main.nw:145 + function Controller:actions (q) + local l = self.actionlist + local i = 0 + return function () + i = i + 1 + return l[i] + end + end +-- ../src/class/controller.nw:22 + +-- ../src/main.nw:56 + function Controller:add_action (a) + assert( a ~= nil, "lemock internal error" ) -- breaks array property + table.insert( self.actionlist, a ) + end +-- ../src/class/controller.nw:23 + +-- ../src/restrictions.nw:489 + 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 +-- ../src/class/controller.nw:24 + +-- ../src/restrictions.nw:616 + 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 + +-- ../src/class/controller.nw:25 + +-- ../src/main.nw:177 + function Controller:get_last_action () + local l = self.actionlist + if #l == 0 then + error( "No action is recorded yet.", 0 ) + end + return l[#l] + end +-- ../src/class/controller.nw:26 + +-- ../src/main.nw:88 + function Controller:lookup (actual) + for action in self:actions() do + if action:match( actual ) then + return action + end + end + +-- ../src/main.nw:111 + local expected = {} + for _, a in ipairs( self.actionlist ) do + if a:is_expected() and not a.is_callable then + expected[#expected+1] = a:tostring() + end + end + table.sort( expected ) + if #expected == 0 then + expected[1] = "(Nothing)" + end +-- ../src/main.nw:95 + error( sfmt( "Unexpected action %s, expected:\n%s\n" + , actual:tostring() + , table.concat(expected,'\n') + ) + , 0 + ) + end +-- ../src/class/controller.nw:27 + +-- ../src/main.nw:531 + function Controller:make_callable (action) + if action.has_returnvalue then + error( "Can not call "..action.key..". It has a returnvalue.", 0 ) + end + action.is_callable = true + action.min_replays = 0 + action.max_replays = math.huge + end +-- ../src/class/controller.nw:28 + +-- ../src/main.nw:421 + function Controller:new () + local mc = object( self ) + mc.actionlist = {} + mc.is_recording = true + return mc + end +-- ../src/class/controller.nw:29 + +-- ../src/main.nw:297 + function Controller:replay_action ( action ) + assert( action:is_expected(), "lemock internal error" ) + assert( action.replay_count < action.max_replays, "lemock internal error" ) + local was_satisfied = action:is_satisfied() + action.replay_count = action.replay_count + 1 + if not was_satisfied and action.labellist and action:is_satisfied() then + self:update_dependencies() + end + if action.closelist then + self:close_actions( action:closes() ) + end + end +-- ../src/class/controller.nw:30 + +-- ../src/restrictions.nw:457 + 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 +-- ../src/unittestfiles.nw:60 + +-- ../src/class/action.nw:24 + Action = {} + + -- abstract + +-- ../src/class/action.nw:41 + Action.generic = class() + + +-- ../src/restrictions.nw:607 + function Action.generic:add_close (label) + add_to_set( self, 'closelist', label ) + end +-- ../src/class/action.nw:44 + +-- ../src/restrictions.nw:443 + function Action.generic:add_depend (d) + add_to_set( self, 'dependlist', d ) + end + +-- ../src/class/action.nw:45 + +-- ../src/restrictions.nw:207 + function Action.generic:add_label (label) + add_to_set( self, 'labellist', label ) + end + +-- ../src/class/action.nw:46 + +-- ../src/main.nw:338 + function Action.generic:assert_satisfied () + assert( self.replay_count <= self.max_replays, "lemock internal error" ) + if not ( +-- ../src/main.nw:330 + self.min_replays <= self.replay_count + +-- ../src/main.nw:340 + ) then + error( sfmt( "Wrong replay count %d (expected %d..%d) for %s" + , self.replay_count + , self.min_replays, self.max_replays + , self:tostring() + ) + , 0 + ) + end + end +-- ../src/class/action.nw:47 + +-- ../src/restrictions.nw:220 + function Action.generic:blocks () + if self:is_satisfied() then + return function () end + end + return elements_of_set( self, 'labellist' ) + end +-- ../src/class/action.nw:48 + +-- ../src/restrictions.nw:630 + function Action.generic:closes () + return elements_of_set( self, 'closelist' ) + end +-- ../src/class/action.nw:49 + +-- ../src/restrictions.nw:448 + function Action.generic:depends () + return elements_of_set( self, 'dependlist' ) + end +-- ../src/class/action.nw:50 + +-- ../src/restrictions.nw:212 + 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 + +-- ../src/class/action.nw:51 + +-- ../src/main.nw:247 + function Action.generic:is_expected () + return self.replay_count < self.max_replays + and not self.is_blocked + and not self.is_closed + end + +-- ../src/class/action.nw:52 + +-- ../src/main.nw:333 + function Action.generic:is_satisfied () + return +-- ../src/main.nw:330 + self.min_replays <= self.replay_count + +-- ../src/main.nw:335 + end + +-- ../src/class/action.nw:53 + +-- ../src/main.nw:269 + function Action.generic:match (key) + if getmetatable(self) ~= getmetatable(key) then return false end + if self.mock ~= key.mock then return false end + return self:is_expected() + end +-- ../src/class/action.nw:54 + +-- ../src/main.nw:219 + function Action.generic:new (mock) + local a = object( self ) + a.mock = mock + a.replay_count = 0 + a.min_replays = 1 + a.max_replays = 1 + return a + end +-- ../src/class/action.nw:55 + +-- ../src/restrictions.nw:102 + 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 + + +-- ../src/class/action.nw:28 + +-- ../src/class/action.nw:59 + Action.generic_call = class( Action.generic ) + + Action.generic_call.can_return = true + +-- ../src/action/generic_call.nw:76 + function Action.generic_call:get_returnvalue () + if self.has_returnvalue then + return self.returnvalue:unpack() + end + end +-- ../src/class/action.nw:63 + +-- ../src/action/generic_call.nw:56 + function Action.generic_call:set_returnvalue (...) + self.returnvalue = Argv:new(...) + self.has_returnvalue = true + end +-- ../src/class/action.nw:64 + + +-- ../src/action/generic_call.nw:45 + function Action.generic_call:match (q) + if not Action.generic.match( self, q ) then return false end + if not self.argv:equal( q.argv ) then return false end + return true + end +-- ../src/class/action.nw:66 + +-- ../src/action/generic_call.nw:32 + function Action.generic_call:new (m, ...) + local a = Action.generic.new( self, m ) + a.argv = Argv:new(...) + return a + end +-- ../src/class/action.nw:29 + + -- concrete + +-- ../src/class/action.nw:93 + Action.call = class( Action.generic_call ) + + +-- ../src/action/call.nw:118 + function Action.call:match (q) + if not Action.generic_call.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true + end +-- ../src/class/action.nw:96 + +-- ../src/action/call.nw:82 + function Action.call:new (m, key, ...) + local a = Action.generic_call.new( self, m, ... ) + a.key = key + return a + end +-- ../src/class/action.nw:97 + +-- ../src/tostring.nw:101 + function Action.call:tostring () + if self.has_returnvalue then + return sfmt( "call %s(%s) => %s" + , tostring(self.key) + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "call %s(%s)" + , tostring(self.key) + , self.argv:tostring() + ) + end + end + + +-- ../src/class/action.nw:32 + +-- ../src/class/action.nw:81 + Action.index = class( Action.generic ) + + Action.index.can_return = true + +-- ../src/action/index.nw:134 + function Action.index:get_returnvalue () + return self.returnvalue + end +-- ../src/class/action.nw:85 + +-- ../src/action/index.nw:85 + function Action.index:set_returnvalue (v) + self.returnvalue = v + self.has_returnvalue = true + end +-- ../src/class/action.nw:86 + + +-- ../src/action/index.nw:123 + function Action.index:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true + end +-- ../src/class/action.nw:88 + +-- ../src/action/index.nw:67 + function Action.index:new (m, key) + local a = Action.generic.new( self, m ) + a.key = key + return a + end +-- ../src/class/action.nw:89 + +-- ../src/tostring.nw:70 + function Action.index:tostring () + local key = 'index '..tostring( self.key ) + if self.has_returnvalue then + return sfmt( "index %s => %s" + , tostring( self.key ) + , qtostring( self.returnvalue ) + ) + elseif self.is_callable then + return sfmt( "index %s()" + , tostring( self.key ) + ) + else + return sfmt( "index %s" + , tostring( self.key ) + ) + end + end + + +-- ../src/class/action.nw:33 + +-- ../src/class/action.nw:73 + Action.newindex = class( Action.generic ) + + +-- ../src/action/newindex.nw:102 + function Action.newindex:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + if not value_equal( self.val, q.val ) + and self.val ~= Argv.ANYARG + and q.val ~= Argv.ANYARG then return false end + return true + end +-- ../src/class/action.nw:76 + +-- ../src/action/newindex.nw:54 + function Action.newindex:new (m, key, val) + local a = Action.generic.new( self, m ) + a.key = key + a.val = val + return a + end +-- ../src/class/action.nw:77 + +-- ../src/tostring.nw:45 + function Action.newindex:tostring () + return sfmt( "newindex %s = %s" + , tostring(self.key) + , qtostring(self.val) + ) + end + + +-- ../src/class/action.nw:34 + +-- ../src/class/action.nw:101 + Action.selfcall = class( Action.generic_call ) + + +-- ../src/action/selfcall.nw:93 + function Action.selfcall:match (q) + return Action.generic_call.match( self, q ) + end +-- ../src/class/action.nw:104 + +-- ../src/action/selfcall.nw:61 + function Action.selfcall:new (m, ...) + local a = Action.generic_call.new( self, m, ... ) + return a + end +-- ../src/class/action.nw:105 + +-- ../src/tostring.nw:129 + function Action.selfcall:tostring () + if self.has_returnvalue then + return sfmt( "selfcall (%s) => %s" + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "selfcall (%s)" + , self.argv:tostring() + ) + end + end +-- ../src/unittestfiles.nw:61 + + local A = Action.generic + Action = nil -- only allow generic action + function A:tostring () return '' end + + local mc + + function setup () + mc = Controller:new() + end + + +-- ../src/main.nw:125 + function actions_dont_iterate_empty_list_test () + for a in mc:actions() do + fail( "iterates on empty list" ) + end + end + function actions_iterate_over_entire_list_exactly_once_test () + local l = { {},{},{} } + for _, a in ipairs( l ) do + mc:add_action( a ) + end + for a in mc:actions() do + assert_nil( a.check ) + a.check = true + end + for _, a in ipairs( l ) do + assert_true( a.check ) + end + end + +-- ../src/unittestfiles.nw:73 + +-- ../src/main.nw:48 + function add_action_at_the_end_test () + mc:add_action( 7 ) + mc:add_action( mc ) + assert_equal( 7, mc.actionlist[1] ) + assert_equal( mc, mc.actionlist[2] ) + end + +-- ../src/unittestfiles.nw:74 + +-- ../src/main.nw:162 + function get_last_action_returns_last_element_test () + local l = { 'a', 'foo', 17 } + for i = 1, #l do + mc:add_action( l[i] ) + local res = mc:get_last_action() + assert_equal( l[i], res ) + end + end + function get_last_action_fails_on_empty_list_test () + local ok, err = pcall( function() mc:get_last_action() end ) + assert_false( ok, "Found last action in empty list" ) + assert_match( "No action is recorded yet", err ) + end + +-- ../src/unittestfiles.nw:75 + +-- ../src/restrictions.nw:143 + 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 + +-- ../src/unittestfiles.nw:76 + +-- ../src/main.nw:71 + function lookup_returns_first_matching_action_test () + local Fake_action + +-- ../src/misc.nw:12 + Fake_action = class() + function Fake_action:new (x) + local a = object(Fake_action) + a.x = x + return a + end + function Fake_action:match (q) + return self.x < q.x + end + function Fake_action:is_expected () + return true + end + function Fake_action:tostring () + return '' + end + function Fake_action:blocks () + return function () end + end + Fake_action.depends = Fake_action.blocks +-- ../src/main.nw:74 + local a1 = Fake_action:new(1) + local a2 = Fake_action:new(2) + local a3 = Fake_action:new(1) + local ok, err = pcall( function() mc:lookup( a1 ) end ) + assert_false( ok, "match in empty list" ) + assert_match( "Unexpected action ", err ) + mc:add_action( a1 ) mc:add_action( a2 ) mc:add_action( a3 ) + local ok, err = pcall( function() mc:lookup( a1 ) end ) + assert_false( ok, "should not match any action" ) + assert_match( "Unexpected action ", err ) + assert_equal( a1, mc:lookup( a2 ), "did not find first match" ) + end + +-- ../src/unittestfiles.nw:77 + +-- ../src/main.nw:664 + function replay_test () + assert_true( mc.is_recording ) + mc:replay() + assert_false( mc.is_recording ) + end + +-- ../src/unittestfiles.nw:78 + +-- ../src/main.nw:285 + function replay_action_test () + local a = A:new() + mc:add_action( a ) + assert_true( a:is_expected() ) + assert_false( a:is_satisfied() ) + mc:replay_action( a ) + assert_false( a:is_expected() ) + assert_true( a:is_satisfied() ) + assert_equal( 1, a.replay_count ) + end + diff --git a/build/unit/module.lua b/build/unit/module.lua new file mode 100644 index 0000000..79bf8c1 --- /dev/null +++ b/build/unit/module.lua @@ -0,0 +1,601 @@ +-- ../src/unittestfiles.nw:10 + +-- ../src/misc.nw:7 + ------ THIS FILE IS TANGLED FROM LITERATE SOURCE FILES ------ + -- Copyright (C) 2009 Tommy Pettersson + -- See terms in file COPYRIGHT, or at http://lemock.luaforge.net + +-- ../src/unittestfiles.nw:11 + + require 'lunit' + module( 'unit.module', lunit.testcase, package.seeall ) + + require 'lemock' + + local mc, m + + function setup () + mc = lemock.controller() + m = mc:mock() + end + + +-- ../src/restrictions.nw:537 + 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 + +-- ../src/restrictions.nw:573 + 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 +-- ../src/unittestfiles.nw:25 + +-- ../src/restrictions.nw:240 + 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 +-- ../src/restrictions.nw:344 + 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 +-- ../src/restrictions.nw:361 + 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 + +-- ../src/restrictions.nw:373 + 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 +-- ../src/unittestfiles.nw:26 + +-- ../src/restrictions.nw:130 + 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 + +-- ../src/unittestfiles.nw:27 + +-- ../src/main.nw:510 + function returns_on_empty_list_fails_test () + local ok, err = pcall( function() mc:returns(nil) end ) + assert_false( ok, "returns called on nothing" ) + assert_match( "No action is recorded yet.", err ) + end + function returns_make_call_fail_test () + local tmp = m.foo ;mc:returns(1) + local ok, err = pcall( function() tmp(2) end ) + assert_false( ok, "called index with returnvalue" ) + assert_match( "Can not call foo. It has a returnvalue.", err ) + end + function callable_index_replays_anytimes_test () + local tmp = m.foo() + mc:replay() + tmp = m.foo + tmp = m.foo + tmp = m.foo() + mc:verify() + end + +-- ../src/unittestfiles.nw:28 + +-- ../src/main.nw:449 + function create_completely_empty_mock_test () + for k, v in pairs( m ) do + fail( "Mock should be empty but contains "..tostring(k) ) + end + end + function create_mock_during_replay_fails_test () + mc:replay() + local ok, err = pcall( function() mc:mock() end ) + assert_false( ok, "mock() succeeded" ) + assert_match( "New mock during replay.", err ) + end + +-- ../src/unittestfiles.nw:29 + +-- ../src/main.nw:692 + function replay_in_any_order_test () + m.a = 1 + m.b = 2 + m.c = 3 + mc:replay() + m.c = 3 + m.a = 1 + m.b = 2 + mc:verify() + end + function replaying_unexpected_action_fails_test () + mc:replay() + local ok, err = pcall( function() m:somethingelse() end ) + assert_false( ok, "unexpected replay succeeded" ) + assert_match( "Unexpected action index somethingelse", err ) + end +-- ../src/main.nw:718 + function cached_recording_callable_fails_during_replay_test () + local tmp = m.foo ; tmp() + mc:replay() + local ok, err = pcall( function() tmp() end ) + assert_false( ok, "Cached callable not detected" ) + assert_match( "client uses cached callable from recording", err ) + end +-- ../src/unittestfiles.nw:30 + +-- ../src/main.nw:642 + function replay_twice_fails_test () + mc:replay() + local ok, err = pcall( function() mc:replay() end ) + assert_false( ok, "replay succeeded twice" ) + assert_match( "Replay called twice.", err ) + end + function multiple_controllers_test () + local mc2 = lemock.controller() + local m2 = mc2:mock() + + -- m -- -- m2 -- + m.foo = 1 + mc:replay() + m2.bar = 2 + m.foo = 1 + mc2:replay() + mc:verify() + m2.bar = 2 + mc2:verify() + end + +-- ../src/unittestfiles.nw:31 + +-- ../src/restrictions.nw:38 + 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 + +-- ../src/unittestfiles.nw:32 + +-- ../src/main.nw:736 + function verify_during_record_phase_fails_test () + local ok, err = pcall( function() mc:verify() end ) + assert_false( ok, "Verify succeeded" ) + assert_match( "Verify called during record.", err ) + end + function verify_replayed_actionlist_test () + mc:replay() + mc:verify() + end + function verify_unreplyed_actionlist_fails_test () + local tmp = m.foo + mc:replay() + local ok, err = pcall( function() mc:verify() end ) + assert_false( ok, "Verify succeeded" ) + assert_match( "Wrong replay count 0 ", err ) + end + +-- ../src/unittestfiles.nw:33 + + +-- ../src/action/call.nw:13 + function call_test () + m.foo(1,2,3) + mc:replay() + local tmp = m.foo(1,2,3) + assert_nil( tmp ) + mc:verify() + end + function call_anyarg_test () + m.foo(1,mc.ANYARG,3) + mc:replay() + local tmp = m.foo(1,2,3) + mc:verify() + end + function call_anyargs_test () + m.foo(mc.ANYARGS) + mc:replay() + local tmp = m.foo(1,2,3) + mc:verify() + end + function call_anyargs_bad_fails_test () + local ok, err = pcall( function() m.foo(mc.ANYARGS, 1) end ) + assert_false( ok, "ANYARGS misused" ) + assert_match( "ANYARGS not at end", err ) + end + function call_return_test () + m.foo(1,2,3) ;mc:returns( 0, 9 ) + mc:replay() + local tmp1, tmp2 = m.foo(1,2,3) + assert_equal( 0, tmp1 ) + assert_equal( 9, tmp2 ) + mc:verify() + end + function call_wrong_name_fails_test () + m.foo(1,2,3) ;mc:returns( 0 ) + mc:replay() + local ok, err = pcall( function() m:bar(1,2,3) end ) + assert_false( ok, "replay wrong index" ) + assert_match( "Unexpected action index bar", err ) + end + function call_wrong_arg_fails_test () + m.foo(1,2,3) ;mc:returns( 0 ) + mc:replay() + local ok, err = pcall( function() m.foo(1) end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action call foo", err ) + end + function call_throws_error_test () + m.boo('Ba') ;mc:error( "Call throws error" ) + mc:replay() + local ok, err = pcall( function() m.boo('Ba') end ) + assert_false( ok, "did not throw error" ) + assert_match( "Call throws error", err ) + end +-- ../src/unittestfiles.nw:35 + +-- ../src/main.nw:596 + function error_during_replay_fails_test () + local tmp = m.foo + mc:replay() + local ok, err = pcall( function() mc:error(1) end ) + assert_false( ok, "error() succeeded during replay" ) + assert_match( "Error called during replay.", err ) + end + function error_twice_fails_test () + local tmp = m.foo ;mc:error(1) + local ok, err = pcall( function() mc:error(2) end ) + assert_false( ok, "duplicate error() succeeded" ) + assert_match( "Returns and/or Error called twice for same action.", err ) + end + function error_plus_returns_fails_test () + local tmp = m.foo ;mc:returns(1) + local ok, err = pcall( function() mc:error(2) end ) + assert_false( ok, "both error and returns succeeded" ) + assert_match( "Returns and/or Error called twice for same action.", err ) + end + +-- ../src/unittestfiles.nw:36 + +-- ../src/action/index.nw:13 + function index_test () + local tmp = m.foo + mc:replay() + local tmp = m.foo + assert_nil( tmp ) + mc:verify() + end + function index_returns_test () + local tmp = m.foo ;mc:returns( 1 ) + mc:replay() + local tmp = m.foo + assert_equal( 1, tmp ) + mc:verify() + end + function index_wrong_key_fails_test () + local tmp = m.foo ;mc:returns( 1 ) + mc:replay() + local ok, err = pcall( function() local tmp = m.bar end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action index bar", err ) + end + function index_throws_error_test () + local tmp = m.foo ;mc:error( "Index throws error" ) + mc:replay() + local ok, err = pcall( function() tmp = m.foo end ) + assert_false( ok, "did not throw error" ) + assert_match( "Index throws error", err ) + end +-- ../src/unittestfiles.nw:37 + +-- ../src/action/newindex.nw:9 + function newindex_test () + m.foo = 1 + mc:replay() + m.foo = 1 + mc:verify() + end + function newindex_anyarg_test () + m.foo = mc.ANYARG + mc:replay() + m.foo = 1 + mc:verify() + end + function newindex_wrong_key_fails_test () + m.foo = 1 + mc:replay() + local ok, err = pcall( function() m.bar = 1 end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action newindex", err ) + end + function newindex_wrong_value_fails_test () + m.foo = 1 + mc:replay() + local ok, err = pcall( function() m.foo = 0 end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action newindex foo", err ) + end + function newindex_throws_error_test () + m.foo = 1 ;mc:error( "newindex throws error" ) + mc:replay() + local ok, err = pcall( function() m.foo = 1 end ) + assert_false( ok, "did not throw error" ) + assert_match( "newindex throws error", err ) + end +-- ../src/unittestfiles.nw:38 + +-- ../src/main.nw:550 + function returns_during_replay_fails_test () + local tmp = m.foo + mc:replay() + local ok, err = pcall( function() mc:returns(1) end ) + assert_false( ok, "returns() succeeded during replay" ) + assert_match( "Returns called during replay.", err ) + end + function returns_on_nonreturning_action_fails_test () + m.foo = 1 -- assignments can't return + local ok, err = pcall( function() mc:returns(0) end ) + assert_false( ok, "returns() succeeded on non-returning action" ) + assert_match( "Previous action can not return anything.", err ) + end + function returns_twice_fails_test () + local tmp = m.foo ;mc:returns(1) + local ok, err = pcall( function() mc:returns(2) end ) + assert_false( ok, "duplicate returns() succeeded" ) + assert_match( "Returns and/or Error called twice for same action.", err ) + end + +-- ../src/unittestfiles.nw:39 + +-- ../src/action/selfcall.nw:12 + function selfcall_test () + m(11) + mc:replay() + local tmp = m(11) + assert_nil( tmp ) + mc:verify() + end + function selfcall_returns_test () + m(99) ;mc:returns(1,nil,'foo') + mc:replay() + local a,b,c = m(99) + assert_equal( 1, a ) + assert_equal( nil, b ) + assert_equal( 'foo', c ) + mc:verify() + end + function selfcall_wrong_argument_fails_test () + m(99) ;mc:returns('a','b','c') + mc:replay() + local ok, err = pcall( function() m(90) end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action selfcall", err ) + end + function selfcall_wrong_number_of_arguments_fails_test () + m(1,2,3) + mc:replay() + local ok, err = pcall( function() m(1,2,3,4) end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action selfcall", err ) + end + function selfcall_throws_error_test () + m('Ba') ;mc:error( "Selfcall throws error" ) + mc:replay() + local ok, err = pcall( function() m('Ba') end ) + assert_false( ok, "did not throw error" ) + assert_match( "Selfcall throws error", err ) + end diff --git a/build/unit/userguide.lua b/build/unit/userguide.lua new file mode 100644 index 0000000..4ebd22d --- /dev/null +++ b/build/unit/userguide.lua @@ -0,0 +1,264 @@ +-- ../src/doc/userguide/unittests.nw:7 + +-- ../src/misc.nw:7 + ------ THIS FILE IS TANGLED FROM LITERATE SOURCE FILES ------ + -- Copyright (C) 2009 Tommy Pettersson + -- See terms in file COPYRIGHT, or at http://lemock.luaforge.net + +-- ../src/doc/userguide/unittests.nw:8 + + require 'lunit' + module( 'unit.userguide', lunit.testcase, package.seeall ) + + +-- ../src/doc/userguide/section_actions.nw:32 + function actions_test () + +-- ../src/doc/userguide/section_actions.nw:20 +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +m.x = 17 -- assignment +r = m.x -- indexing +m.x(1,2,3) -- method call +m:x(1,2,3) -- method call +m(1,2,3) -- self call +-- ../src/doc/userguide/section_actions.nw:34 + end +-- ../src/doc/userguide/unittests.nw:13 + +-- ../src/doc/userguide/section_anyargs.nw:42 + function example_anyargs_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + function foo.fetch_data (con) + local res = con:poll() + while not res do + con:sleep( 10 ) + res = con:poll() + end + con.lasttime = os.time() + return tonumber( res ) + end + end + +-- ../src/doc/userguide/section_anyargs.nw:24 +require 'lemock' +local mc = lemock.controller() +local con = mc:mock() + +con:poll() ;mc :returns(nil) +con:sleep(mc.ANYARG) +con:poll() ;mc :returns('123.45') +con.lasttime = mc.ANYARG + +mc:replay() +require 'foo' +local res = foo.fetch_data(con) +assert( math.abs(res-123.45) < 0.0005 ) + +mc:verify() +-- ../src/doc/userguide/section_anyargs.nw:57 + end +-- ../src/doc/userguide/unittests.nw:14 + +-- ../src/doc/userguide/section_close.nw:53 + function close_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + function foo.dump (xio, name, len) + local f = xio.open( name, 'r' ) + f:read( len ) + f:close() + end + end + +-- ../src/doc/userguide/section_close.nw:31 +require 'lemock' +local mc = lemock.controller() +local myio = mc:mock() +local fs = mc:mock() + +myio.open('abc', 'r') ;mc :returns(fs) +mc :label('open') + +fs:read(mc.ANYARG) ;mc :returns('data') +mc :atleastonce() :label('read') :depend('open') + +fs:close() ;mc :returns(true) +mc :depend('open') :close('read') + +mc:replay() +require 'foo' +foo.dump(myio, 'abc', 128) + +mc:verify() +-- ../src/doc/userguide/section_close.nw:64 + end +-- ../src/doc/userguide/unittests.nw:15 + +-- ../src/doc/userguide/section_label_depend.nw:57 + function example_depend_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + function foo.draw_square (sq) + sq:botright() sq:topright() sq:rightedge() + sq:botleft() sq:topleft() sq:leftedge() + sq:topedge() sq:botedge() + sq:fill() + end + end + +-- ../src/doc/userguide/section_label_depend.nw:35 +require 'lemock' +local mc = lemock.controller() +local square = mc:mock() + +square:topleft() ;mc :label('tl') +square:topright() ;mc :label('tr') +square:botleft() ;mc :label('bl') +square:botright() ;mc :label('br') +square:leftedge() ;mc :label('edge') :depend('tl', 'bl') +square:rightedge() ;mc :label('edge') :depend('tr', 'br') +square:topedge() ;mc :label('edge') :depend('tl', 'tr') +square:botedge() ;mc :label('edge') :depend('bl', 'br') +square:fill() ;mc :depend('edge') + +mc:replay() +require 'foo' +foo.draw_square( square ) + +mc:verify() +-- ../src/doc/userguide/section_label_depend.nw:69 + end +-- ../src/doc/userguide/unittests.nw:16 + +-- ../src/doc/userguide/chapter_tricks.nw:65 + function overloading_test () + +-- ../src/doc/userguide/chapter_tricks.nw:39 +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +do +local function add (a, b) + if type(a) == 'number' then + return m.add_number(a, b) + else + return m.add_string(a, b) + end +end +rawset( m, 'add', add ) -- not recorded +end -- do + +m.add_number(1, 2) ;mc :returns(3) +m.add_string('foo', 'bar') ;mc :returns('foobar') + +mc:replay() +assert_equal( 3, m.add(1, 2) ) +assert_equal( 'foobar', m.add('foo', 'bar') ) + +mc:verify() +-- ../src/doc/userguide/chapter_tricks.nw:67 + end +-- ../src/doc/userguide/unittests.nw:17 + +-- ../src/doc/userguide/section_returns_error.nw:36 + function returns_error_test () + +-- ../src/doc/userguide/section_returns_error.nw:27 +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +m:foo(17) ;mc :returns(nil, "index out of range") +m:bar(-1) ;mc :error("invalid index") +-- ../src/doc/userguide/section_returns_error.nw:38 + end +-- ../src/doc/userguide/unittests.nw:18 + +-- ../src/doc/userguide/chapter_introduction.nw:71 + function example_simple_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + q = require 'luasql.sqlite3' + function foo.insert_data() + local env = q() + local con = env:connect( '/data/base' ) + local ok, err = pcall( con.execute, con, 'insert foo bar' ) + con:close() + env:close() + return ok + end + return foo + end + +-- ../src/doc/userguide/chapter_introduction.nw:40 +-- Setup +require 'lemock' +local mc = lemock.controller() +local sqlite3 = mc:mock() +local env = mc:mock() +local con = mc:mock() +package.loaded.luasql = nil +package.preload['luasql.sqlite3'] = function () + luasql = {} + luasql.sqlite3 = sqlite3 + return sqlite3 +end + +-- Record +sqlite3() ;mc :returns(env) +env:connect('/data/base') ;mc :returns(con) +con:execute(mc.ANYARGS) ;mc :error('LuaSQL: no such table') +con:close() +env:close() + +-- Replay +mc:replay() +require 'foo' +local res = foo.insert_data(17) +assert(res==false) + +--Verify +mc:verify() +-- ../src/doc/userguide/chapter_introduction.nw:87 + end +-- ../src/doc/userguide/unittests.nw:19 + +-- ../src/doc/userguide/section_times.nw:52 + function example_times_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + function foo.mk_watcher ( con ) + local o = {} + function o:set ( key, val ) + con:update( key, val ) + end + return o + end + end + +-- ../src/doc/userguide/section_times.nw:36 +require 'lemock' +local mc = lemock.controller() +local con = mc:mock() + +con:log(mc.ANYARGS) ;mc :anytimes() +con:update('x',3) ;mc :returns(true) :atleastonce() + +mc:replay() +require 'foo' +local watcher = foo.mk_watcher( con ) +watcher:set( 'x', 3 ) + +mc:verify() +-- ../src/doc/userguide/section_times.nw:65 + end diff --git a/build/userguide.t2t b/build/userguide.t2t new file mode 100644 index 0000000..0dca50f --- /dev/null +++ b/build/userguide.t2t @@ -0,0 +1,352 @@ +LeMock User Guide +v 0.6 +2009-05-30 + +%!postproc(html): – – +%!postproc(tex): – -- + += Introduction = + +Mock objects replace difficult external objects during unit testing by +simulating the behaviors of the replaced objects. This is done by first +recording actions and their responses with the mock objects, and then +switching to replay mode. During replay mode the mock objects simulate the +replaced objects by looking up actions and replaying the recorded +responses, and finally verifying that all expected actions where completely +replayed. + +Actions are stored in a list in a special controller object. During replay +the list is searched in recording order for the first matching action that +can be replayed. + +Restrictions on the actions can be inserted during the recording phase. An +action can have a maximum count of how many times it will be replayed, and +a minimum count of how many times it must be replayed to be satisfied. An +action can depend on any set of other actions, and can not be replayed +before all of its depended actions are satisfied. An action can close any +set of actions when it is replayed, which stops all further replaying of +the closed actions. This is good for simulating state changes. + +=== Example === + +This example tests that the insert_data function of the foo module handles +a missing data base table gracefully. + +``` +-- Setup +require 'lemock' +local mc = lemock.controller() +local sqlite3 = mc:mock() +local env = mc:mock() +local con = mc:mock() +package.loaded.luasql = nil +package.preload['luasql.sqlite3'] = function () + luasql = {} + luasql.sqlite3 = sqlite3 + return sqlite3 +end + +-- Record +sqlite3() ;mc :returns(env) +env:connect('/data/base') ;mc :returns(con) +con:execute(mc.ANYARGS) ;mc :error('LuaSQL: no such table') +con:close() +env:close() + +-- Replay +mc:replay() +require 'foo' +local res = foo.insert_data(17) +assert(res==false) + +--Verify +mc:verify() +``` + +First a controller is created. Then three mock objects are created, one for +the sqlite3 module, and two for objects returned by the (simulated) module. + +Then a preloader for the sqlite3 module is installed, which returns the +sqlite3 mock object instead of the actual sqlite3 module. + +In the record phase the expected calls and their return values (or thrown +errors) are recorded. The order is not significant, so this simplified test +will not detect if the close method is called before the execute method. + +In the replay phase the tested module is loaded and executed. It will use +the mock objects instead of the real data base, and if it makes any +unrecorded calls, an error is thrown. + +The verify phase asserts that all recorded actions have been replayed. If +the foo module for example forgets to call the close method, verify throws +an error. + += The Mock Object = + +Mock objects are empty objects with special Lua meta methods that detect +actions performed with the object. What happens depends on the state +(recording or replaying) of the controller which created the mock object. +During recording the mock object adds the action to the controller's list +of recorded actions. During replay the mock object looks for a matching +recorded action that can be replayed, and simulates the action. + +Some action attributes can not be inferred by the mock objects, for example +return values. These attributes have to be added afterwards with special +controller methods, and always affect the last recorded action. + +== Actions == + +Mock objects detect four types of actions: assignment, indexing, method +call, and self call. During replay an action will only match if it is the +very same action, that is, the same type of action performed on the same +mock object with all the same arguments. There are however +[special arguments #anyargs] that can be used during recording. + +``` +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +m.x = 17 -- assignment +r = m.x -- indexing +m.x(1,2,3) -- method call +m:x(1,2,3) -- method call +m(1,2,3) -- self call +``` +== Anyargs ==[anyargs] + + +An //anyarg// is a special argument used when recording, that will match +any argument during replay. It can appear anywhere and any times in an +argument list, or as the argument in an assignment, to replace real +arguments. There is also //anyargs//, which will match any number +(including zero) of any arguments. Anyargs can only appear as the last +argument of an argument list. Anyarg and anyargs are handy when the actual +values of the arguments during replay are unimportant or unknown. + +Anyarg and anyargs are constants defined in the controller object. + +=== Example === + +This example tests that the fetch_data function of module foo waits a while +and retries when no data is immediately available, and that it updates the +value of lasttime. + +``` +require 'lemock' +local mc = lemock.controller() +local con = mc:mock() + +con:poll() ;mc :returns(nil) +con:sleep(mc.ANYARG) +con:poll() ;mc :returns('123.45') +con.lasttime = mc.ANYARG + +mc:replay() +require 'foo' +local res = foo.fetch_data(con) +assert( math.abs(res-123.45) < 0.0005 ) + +mc:verify() +``` += The Controller = + +The controller's main purpose is to store the recorded actions, create mock +objects, switch to replay mode, and verify the completion of the replay +phase. But it is also needed to set or change special action attributes +during recording. + +It is possible, although doubtfully useful, to use several controllers in +parallel during a single unit test. Each controller maintains its own +action list and state, and mock objects remember which controller they +belong to. + +== Returns & Error == + +The by far most useful special action attribute is the return value. +Indexing actions can return a single value, while call actions and self +call actions can return a list of values. The return value is set with the +//returns// method, and it is an error to set the return value twice for +the same action. + +For purposes of unit testing it is often useful to simulate errors. All +actions can raise an error, and return an error value (usually a string). +The return value is set with the //error// method. An action can not have +both a return value and raise an error. + +=== Example === +``` +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +m:foo(17) ;mc :returns(nil, "index out of range") +m:bar(-1) ;mc :error("invalid index") +``` +== Label & Depend == + +Dependencies block actions from replaying until other actions have replayed +first. They can be used to verify that actions are being replayed in a +valid order. + +To add dependencies, actions must first be labeled with one or more +//labels//. The same label can be given to several actions. As long as some +action with the label remains unsatisfied, that label is blocked, and all +actions depending on that label will not replay. + +=== Example === + +This (contrived) example tests that function draw_square in module foo +calls all the necessary drawing methods of a square object in a correct +order. Note that there can be more than one correct order. + +``` +require 'lemock' +local mc = lemock.controller() +local square = mc:mock() + +square:topleft() ;mc :label('tl') +square:topright() ;mc :label('tr') +square:botleft() ;mc :label('bl') +square:botright() ;mc :label('br') +square:leftedge() ;mc :label('edge') :depend('tl', 'bl') +square:rightedge() ;mc :label('edge') :depend('tr', 'br') +square:topedge() ;mc :label('edge') :depend('tl', 'tr') +square:botedge() ;mc :label('edge') :depend('bl', 'br') +square:fill() ;mc :depend('edge') + +mc:replay() +require 'foo' +foo.draw_square( square ) + +mc:verify() +``` + +This example demonstrates two different ways of using dependencies. All the +corners have unique labels, because each edge depend on a set of specific +corners. But all the edges have the same label, because the fill operation +only depends on //all// edges have been satisfied. +== Times == + +The default for a recorded action is to be replayed exactly once. +``times(2)`` changes that to exactly two times, and ``times(1,2)`` changes +it to at least one time and at most two times. + +When the action has been replayed the least count times it is +//satisfied//, which means verify will not complain about it, and it no +longer blocks actions that depend on this action from being replayed. If +the least count is zero the action is automatically satisfied and need not +be replayed at all, i.e., it is optional. + +When the action has been replayed the most count times it will not replay +any more. The most replay count can be set to infinity (``math.huge`` or +``1/0``), in which case the action will never stop replaying. + +``anytimes()`` can be used as an alias for ``times(0,1/0)``, and +``atleastonce()`` can be used as an alias for ``times(1,1/0)``. + +=== Example === + +This example tests that method update is called at least once. + +``` +require 'lemock' +local mc = lemock.controller() +local con = mc:mock() + +con:log(mc.ANYARGS) ;mc :anytimes() +con:update('x',3) ;mc :returns(true) :atleastonce() + +mc:replay() +require 'foo' +local watcher = foo.mk_watcher( con ) +watcher:set( 'x', 3 ) + +mc:verify() +``` +== Close == + +Close can be used to simulate state changes in a limited way. When an +action with a close statement is replayed for the first time, it will +permanently block all labels in its close statement, so that actions with +these labels no longer replays. This passes on matching to later actions in +the action list, which may for example have different return values. + +The closing simply blocks the labels, and it has nothing to do with max +replay counts or if closed actions have been satisfied or not. Closing an +unsatisfied action however results in an immediate failure. + +=== Example === + +This example tests that the dump function of module foo calls the myio +functions in a correct order. The read function can be called any number of +times, until it is closed by the close function. + +``` +require 'lemock' +local mc = lemock.controller() +local myio = mc:mock() +local fs = mc:mock() + +myio.open('abc', 'r') ;mc :returns(fs) +mc :label('open') + +fs:read(mc.ANYARG) ;mc :returns('data') +mc :atleastonce() :label('read') :depend('open') + +fs:close() ;mc :returns(true) +mc :depend('open') :close('read') + +mc:replay() +require 'foo' +foo.dump(myio, 'abc', 128) + +mc:verify() +``` += Tricks = + +Mock objects are completely empty, and do not contain any methods or +properties of their own. If they did, that would risk shadowing a name of a +simulated object's method or property. There is however nothing preventing +users from defining methods and properties in mock objects. This way mock +objects can be turned into stubs, or a kind of mock–stub hybrid. + +== Method Overloading == + +Lua does not support method overloading, but it can be (and sometimes is) +implemented manually by testing of function arguments. This presents a +problem to LeMock, because it matches exact arguments, and anyargs in not +sufficient. In this case the mock object can be extended with a dispatcher +function. + +=== Example === + +This example shows a mock object with an overloaded add function. The stub +function can not be defined in the usual way, because that would record an +assignment action; it needs to be defined with //rawset//. + +``` +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +do +local function add (a, b) + if type(a) == 'number' then + return m.add_number(a, b) + else + return m.add_string(a, b) + end +end +rawset( m, 'add', add ) -- not recorded +end -- do + +m.add_number(1, 2) ;mc :returns(3) +m.add_string('foo', 'bar') ;mc :returns('foobar') + +mc:replay() +assert_equal( 3, m.add(1, 2) ) +assert_equal( 'foobar', m.add('foo', 'bar') ) + +mc:verify() +``` diff --git a/build/www/COPYRIGHT.t2t b/build/www/COPYRIGHT.t2t new file mode 100644 index 0000000..01d741d --- /dev/null +++ b/build/www/COPYRIGHT.t2t @@ -0,0 +1,11 @@ +LeMock + + +%!includeconf: config.rc +%!preproc: COPYRIGHT_ "[COPYRIGHT COPYRIGHT.html]" +%!preproc: DEVEL_ "[DEVEL DEVEL.html]" +%!preproc: "build/htdocs/userguide.html" "[the user guide userguide.html]" +%!postproc(html): 'ID="body"' 'ID="page-COPYRIGHT"' +%!include(html): ''menubar.html'' += License = +%!include: ../../COPYRIGHT diff --git a/build/www/DEVEL.t2t b/build/www/DEVEL.t2t new file mode 100644 index 0000000..51b8413 --- /dev/null +++ b/build/www/DEVEL.t2t @@ -0,0 +1,13 @@ +LeMock + + +%!includeconf: config.rc +%!preproc: COPYRIGHT_ "[COPYRIGHT COPYRIGHT.html]" +%!preproc: DEVEL_ "[DEVEL DEVEL.html]" +%!preproc: "build/htdocs/userguide.html" "[the user guide userguide.html]" +%!postproc(html): 'ID="body"' 'ID="page-DEVEL"' +%!include(html): ''menubar.html'' += Developer Notes = +%!include: ../../DEVEL +-------------------- +%%Date(%Y-%m-%d) diff --git a/build/www/HISTORY.t2t b/build/www/HISTORY.t2t new file mode 100644 index 0000000..f29f930 --- /dev/null +++ b/build/www/HISTORY.t2t @@ -0,0 +1,13 @@ +LeMock + + +%!includeconf: config.rc +%!preproc: COPYRIGHT_ "[COPYRIGHT COPYRIGHT.html]" +%!preproc: DEVEL_ "[DEVEL DEVEL.html]" +%!preproc: "build/htdocs/userguide.html" "[the user guide userguide.html]" +%!postproc(html): 'ID="body"' 'ID="page-HISTORY"' +%!include(html): ''menubar.html'' += History = +%!include: ../../HISTORY +-------------------- +%%Date(%Y-%m-%d) diff --git a/build/www/README.t2t b/build/www/README.t2t new file mode 100644 index 0000000..f2c0c70 --- /dev/null +++ b/build/www/README.t2t @@ -0,0 +1,13 @@ +LeMock + + +%!includeconf: config.rc +%!preproc: COPYRIGHT_ "[COPYRIGHT COPYRIGHT.html]" +%!preproc: DEVEL_ "[DEVEL DEVEL.html]" +%!preproc: "build/htdocs/userguide.html" "[the user guide userguide.html]" +%!postproc(html): 'ID="body"' 'ID="page-README"' +%!include(html): ''menubar.html'' += Readme = +%!include: ../../README +-------------------- +%%Date(%Y-%m-%d) diff --git a/build/www/config.rc b/build/www/config.rc new file mode 100644 index 0000000..5938508 --- /dev/null +++ b/build/www/config.rc @@ -0,0 +1,3 @@ +%!options: --no-rc +%!style(html): style.css +%!options(html): --css-sugar diff --git a/build/www/index.t2t b/build/www/index.t2t new file mode 100644 index 0000000..46eeada --- /dev/null +++ b/build/www/index.t2t @@ -0,0 +1,9 @@ +LeMock + + +%!includeconf: config.rc +%!preproc: COPYRIGHT_ "[COPYRIGHT COPYRIGHT.html]" +%!preproc: DEVEL_ "[DEVEL DEVEL.html]" +%!preproc: "build/htdocs/userguide.html" "[the user guide userguide.html]" +%!postproc(html): 'ID="body"' 'ID="page-index"' +%!include(html): ''menubar.html'' diff --git a/build/www/menubar.html b/build/www/menubar.html new file mode 100644 index 0000000..4c3e4d3 --- /dev/null +++ b/build/www/menubar.html @@ -0,0 +1,7 @@ + diff --git a/build/www/userguide.t2t b/build/www/userguide.t2t new file mode 100644 index 0000000..600f959 --- /dev/null +++ b/build/www/userguide.t2t @@ -0,0 +1,13 @@ +LeMock + + +%!includeconf: config.rc +%!preproc: COPYRIGHT_ "[COPYRIGHT COPYRIGHT.html]" +%!preproc: DEVEL_ "[DEVEL DEVEL.html]" +%!preproc: "build/htdocs/userguide.html" "[the user guide userguide.html]" +%!postproc(html): 'ID="body"' 'ID="page-userguide"' +%!include(html): ''menubar.html'' +%%toc +%!include: ../userguide.t2t +-------------------- +%%Date(%Y-%m-%d) diff --git a/cmake/FindLua.cmake b/cmake/FindLua.cmake new file mode 100644 index 0000000..7fb7ca3 --- /dev/null +++ b/cmake/FindLua.cmake @@ -0,0 +1,118 @@ +# Locate Lua library +# This module defines +# LUA_EXECUTABLE, if found +# LUA_FOUND, if false, do not try to link to Lua +# LUA_LIBRARIES +# LUA_INCLUDE_DIR, where to find lua.h +# LUA_VERSION_STRING, the version of Lua found (since CMake 2.8.8) +# +# Note that the expected include convention is +# #include "lua.h" +# and not +# #include +# This is because, the lua location is not standardized and may exist +# in locations other than lua/ + +#============================================================================= +# Copyright 2007-2009 Kitware, Inc. +# Modified to support Lua 5.2 by LuaDist 2012 +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# (To distribute this file outside of CMake, substitute the full +# License text for the above reference.) +# +# The required version of Lua can be specified using the +# standard syntax, e.g. FIND_PACKAGE(Lua 5.1) +# Otherwise the module will search for any available Lua implementation + +# Always search for non-versioned lua first (recommended) +SET(_POSSIBLE_LUA_INCLUDE include include/lua) +SET(_POSSIBLE_LUA_EXECUTABLE lua) +SET(_POSSIBLE_LUA_LIBRARY lua) + +# Determine possible naming suffixes (there is no standard for this) +IF(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) + SET(_POSSIBLE_SUFFIXES "${Lua_FIND_VERSION_MAJOR}${Lua_FIND_VERSION_MINOR}" "${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR}" "-${Lua_FIND_VERSION_MAJOR}.${Lua_FIND_VERSION_MINOR}") +ELSE(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) + SET(_POSSIBLE_SUFFIXES "52" "5.2" "-5.2" "51" "5.1" "-5.1") +ENDIF(Lua_FIND_VERSION_MAJOR AND Lua_FIND_VERSION_MINOR) + +# Set up possible search names and locations +FOREACH(_SUFFIX ${_POSSIBLE_SUFFIXES}) + LIST(APPEND _POSSIBLE_LUA_INCLUDE "include/lua${_SUFFIX}") + LIST(APPEND _POSSIBLE_LUA_EXECUTABLE "lua${_SUFFIX}") + LIST(APPEND _POSSIBLE_LUA_LIBRARY "lua${_SUFFIX}") +ENDFOREACH(_SUFFIX) + +# Find the lua executable +FIND_PROGRAM(LUA_EXECUTABLE + NAMES ${_POSSIBLE_LUA_EXECUTABLE} +) + +# Find the lua header +FIND_PATH(LUA_INCLUDE_DIR lua.h + HINTS + $ENV{LUA_DIR} + PATH_SUFFIXES ${_POSSIBLE_LUA_INCLUDE} + PATHS + ~/Library/Frameworks + /Library/Frameworks + /usr/local + /usr + /sw # Fink + /opt/local # DarwinPorts + /opt/csw # Blastwave + /opt +) + +# Find the lua library +FIND_LIBRARY(LUA_LIBRARY + NAMES ${_POSSIBLE_LUA_LIBRARY} + HINTS + $ENV{LUA_DIR} + PATH_SUFFIXES lib64 lib + PATHS + ~/Library/Frameworks + /Library/Frameworks + /usr/local + /usr + /sw + /opt/local + /opt/csw + /opt +) + +IF(LUA_LIBRARY) + # include the math library for Unix + IF(UNIX AND NOT APPLE) + FIND_LIBRARY(LUA_MATH_LIBRARY m) + SET( LUA_LIBRARIES "${LUA_LIBRARY};${LUA_MATH_LIBRARY}" CACHE STRING "Lua Libraries") + # For Windows and Mac, don't need to explicitly include the math library + ELSE(UNIX AND NOT APPLE) + SET( LUA_LIBRARIES "${LUA_LIBRARY}" CACHE STRING "Lua Libraries") + ENDIF(UNIX AND NOT APPLE) +ENDIF(LUA_LIBRARY) + +# Determine Lua version +IF(LUA_INCLUDE_DIR AND EXISTS "${LUA_INCLUDE_DIR}/lua.h") + FILE(STRINGS "${LUA_INCLUDE_DIR}/lua.h" lua_version_str REGEX "^#define[ \t]+LUA_RELEASE[ \t]+\"Lua .+\"") + + STRING(REGEX REPLACE "^#define[ \t]+LUA_RELEASE[ \t]+\"Lua ([^\"]+)\".*" "\\1" LUA_VERSION_STRING "${lua_version_str}") + UNSET(lua_version_str) +ENDIF() + +INCLUDE(FindPackageHandleStandardArgs) +# handle the QUIETLY and REQUIRED arguments and set LUA_FOUND to TRUE if +# all listed variables are TRUE +FIND_PACKAGE_HANDLE_STANDARD_ARGS(Lua + REQUIRED_VARS LUA_LIBRARIES LUA_INCLUDE_DIR + VERSION_VAR LUA_VERSION_STRING) + +MARK_AS_ADVANCED(LUA_INCLUDE_DIR LUA_LIBRARIES LUA_LIBRARY LUA_MATH_LIBRARY LUA_EXECUTABLE) + diff --git a/cmake/dist.cmake b/cmake/dist.cmake new file mode 100644 index 0000000..310ef94 --- /dev/null +++ b/cmake/dist.cmake @@ -0,0 +1,321 @@ +# LuaDist CMake utility library. +# Provides sane project defaults and macros common to LuaDist CMake builds. +# +# Copyright (C) 2007-2012 LuaDist. +# by David Manura, Peter Drahoš +# Redistribution and use of this file is allowed according to the terms of the MIT license. +# For details see the COPYRIGHT file distributed with LuaDist. +# Please note that the package source code is licensed under its own license. + +## Extract information from dist.info +if ( NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/dist.info ) + message ( FATAL_ERROR + "Missing dist.info file (${CMAKE_CURRENT_SOURCE_DIR}/dist.info)." ) +endif () +file ( READ ${CMAKE_CURRENT_SOURCE_DIR}/dist.info DIST_INFO ) +if ( "${DIST_INFO}" STREQUAL "" ) + message ( FATAL_ERROR "Failed to load dist.info." ) +endif () +# Reads field `name` from dist.info string `DIST_INFO` into variable `var`. +macro ( _parse_dist_field name var ) + string ( REGEX REPLACE ".*${name}[ \t]?=[ \t]?[\"']([^\"']+)[\"'].*" "\\1" + ${var} "${DIST_INFO}" ) + if ( ${var} STREQUAL DIST_INFO ) + message ( FATAL_ERROR "Failed to extract \"${var}\" from dist.info" ) + endif () +endmacro () +# +_parse_dist_field ( name DIST_NAME ) +_parse_dist_field ( version DIST_VERSION ) +_parse_dist_field ( license DIST_LICENSE ) +_parse_dist_field ( author DIST_AUTHOR ) +_parse_dist_field ( maintainer DIST_MAINTAINER ) +_parse_dist_field ( url DIST_URL ) +_parse_dist_field ( desc DIST_DESC ) +message ( "DIST_NAME: ${DIST_NAME}") +message ( "DIST_VERSION: ${DIST_VERSION}") +message ( "DIST_LICENSE: ${DIST_LICENSE}") +message ( "DIST_AUTHOR: ${DIST_AUTHOR}") +message ( "DIST_MAINTAINER: ${DIST_MAINTAINER}") +message ( "DIST_URL: ${DIST_URL}") +message ( "DIST_DESC: ${DIST_DESC}") +string ( REGEX REPLACE ".*depends[ \t]?=[ \t]?[\"']([^\"']+)[\"'].*" "\\1" + DIST_DEPENDS ${DIST_INFO} ) +if ( DIST_DEPENDS STREQUAL DIST_INFO ) + set ( DIST_DEPENDS "" ) +endif () +message ( "DIST_DEPENDS: ${DIST_DEPENDS}") +## 2DO: Parse DIST_DEPENDS and try to install Dependencies with automatically using externalproject_add + + +## INSTALL DEFAULTS (Relative to CMAKE_INSTALL_PREFIX) +# Primary paths +set ( INSTALL_BIN bin CACHE PATH "Where to install binaries to." ) +set ( INSTALL_LIB lib CACHE PATH "Where to install libraries to." ) +set ( INSTALL_INC include CACHE PATH "Where to install headers to." ) +set ( INSTALL_ETC etc CACHE PATH "Where to store configuration files" ) +set ( INSTALL_SHARE share CACHE PATH "Directory for shared data." ) + +# Secondary paths +option ( INSTALL_VERSION + "Install runtime libraries and executables with version information." OFF) +set ( INSTALL_DATA ${INSTALL_SHARE}/${DIST_NAME} CACHE PATH + "Directory the package can store documentation, tests or other data in.") +set ( INSTALL_DOC ${INSTALL_DATA}/doc CACHE PATH + "Recommended directory to install documentation into.") +set ( INSTALL_EXAMPLE ${INSTALL_DATA}/example CACHE PATH + "Recommended directory to install examples into.") +set ( INSTALL_TEST ${INSTALL_DATA}/test CACHE PATH + "Recommended directory to install tests into.") +set ( INSTALL_FOO ${INSTALL_DATA}/etc CACHE PATH + "Where to install additional files") + +# Tweaks and other defaults +# Setting CMAKE to use loose block and search for find modules in source directory +set ( CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS true ) +set ( CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH} ) +option ( BUILD_SHARED_LIBS "Build shared libraries" ON ) + +# In MSVC, prevent warnings that can occur when using standard libraries. +if ( MSVC ) + add_definitions ( -D_CRT_SECURE_NO_WARNINGS ) +endif () + +# RPath and relative linking +option ( USE_RPATH "Use relative linking." ON) +if ( USE_RPATH ) + string ( REGEX REPLACE "[^!/]+" ".." UP_DIR ${INSTALL_BIN} ) + set ( CMAKE_SKIP_BUILD_RPATH FALSE CACHE STRING "" FORCE ) + set ( CMAKE_BUILD_WITH_INSTALL_RPATH FALSE CACHE STRING "" FORCE ) + set ( CMAKE_INSTALL_RPATH $ORIGIN/${UP_DIR}/${INSTALL_LIB} + CACHE STRING "" FORCE ) + set ( CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE CACHE STRING "" FORCE ) + set ( CMAKE_INSTALL_NAME_DIR @executable_path/${UP_DIR}/${INSTALL_LIB} + CACHE STRING "" FORCE ) +endif () + +## MACROS +# Parser macro +macro ( parse_arguments prefix arg_names option_names) + set ( DEFAULT_ARGS ) + foreach ( arg_name ${arg_names} ) + set ( ${prefix}_${arg_name} ) + endforeach () + foreach ( option ${option_names} ) + set ( ${prefix}_${option} FALSE ) + endforeach () + + set ( current_arg_name DEFAULT_ARGS ) + set ( current_arg_list ) + foreach ( arg ${ARGN} ) + set ( larg_names ${arg_names} ) + list ( FIND larg_names "${arg}" is_arg_name ) + if ( is_arg_name GREATER -1 ) + set ( ${prefix}_${current_arg_name} ${current_arg_list} ) + set ( current_arg_name ${arg} ) + set ( current_arg_list ) + else () + set ( loption_names ${option_names} ) + list ( FIND loption_names "${arg}" is_option ) + if ( is_option GREATER -1 ) + set ( ${prefix}_${arg} TRUE ) + else () + set ( current_arg_list ${current_arg_list} ${arg} ) + endif () + endif () + endforeach () + set ( ${prefix}_${current_arg_name} ${current_arg_list} ) +endmacro () + + +# install_executable ( executable_targets ) +# Installs any executables generated using "add_executable". +# USE: install_executable ( lua ) +# NOTE: subdirectories are NOT supported +set ( CPACK_COMPONENT_RUNTIME_DISPLAY_NAME "${DIST_NAME} Runtime" ) +set ( CPACK_COMPONENT_RUNTIME_DESCRIPTION + "Executables and runtime libraries. Installed into ${INSTALL_BIN}." ) +macro ( install_executable ) + foreach ( _file ${ARGN} ) + if ( INSTALL_VERSION ) + set_target_properties ( ${_file} PROPERTIES VERSION ${DIST_VERSION} + SOVERSION ${DIST_VERSION} ) + endif () + install ( TARGETS ${_file} RUNTIME DESTINATION ${INSTALL_BIN} + COMPONENT Runtime ) + endforeach() +endmacro () + +# install_library ( library_targets ) +# Installs any libraries generated using "add_library" into apropriate places. +# USE: install_library ( libexpat ) +# NOTE: subdirectories are NOT supported +set ( CPACK_COMPONENT_LIBRARY_DISPLAY_NAME "${DIST_NAME} Development Libraries" ) +set ( CPACK_COMPONENT_LIBRARY_DESCRIPTION + "Static and import libraries needed for development. Installed into ${INSTALL_LIB} or ${INSTALL_BIN}." ) +macro ( install_library ) + foreach ( _file ${ARGN} ) + if ( INSTALL_VERSION ) + set_target_properties ( ${_file} PROPERTIES VERSION ${DIST_VERSION} + SOVERSION ${DIST_VERSION} ) + endif () + install ( TARGETS ${_file} + RUNTIME DESTINATION ${INSTALL_BIN} COMPONENT Runtime + LIBRARY DESTINATION ${INSTALL_LIB} COMPONENT Runtime + ARCHIVE DESTINATION ${INSTALL_LIB} COMPONENT Library ) + endforeach() +endmacro () + +# helper function for various install_* functions, for PATTERN/REGEX args. +macro ( _complete_install_args ) + if ( NOT("${_ARG_PATTERN}" STREQUAL "") ) + set ( _ARG_PATTERN PATTERN ${_ARG_PATTERN} ) + endif () + if ( NOT("${_ARG_REGEX}" STREQUAL "") ) + set ( _ARG_REGEX REGEX ${_ARG_REGEX} ) + endif () +endmacro () + +# install_header ( files/directories [INTO destination] ) +# Install a directories or files into header destination. +# USE: install_header ( lua.h luaconf.h ) or install_header ( GL ) +# USE: install_header ( mylib.h INTO mylib ) +# For directories, supports optional PATTERN/REGEX arguments like install(). +set ( CPACK_COMPONENT_HEADER_DISPLAY_NAME "${DIST_NAME} Development Headers" ) +set ( CPACK_COMPONENT_HEADER_DESCRIPTION + "Headers needed for development. Installed into ${INSTALL_INC}." ) +macro ( install_header ) + parse_arguments ( _ARG "INTO;PATTERN;REGEX" "" ${ARGN} ) + _complete_install_args() + foreach ( _file ${_ARG_DEFAULT_ARGS} ) + if ( IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${_file}" ) + install ( DIRECTORY ${_file} DESTINATION ${INSTALL_INC}/${_ARG_INTO} + COMPONENT Header ${_ARG_PATTERN} ${_ARG_REGEX} ) + else () + install ( FILES ${_file} DESTINATION ${INSTALL_INC}/${_ARG_INTO} + COMPONENT Header ) + endif () + endforeach() +endmacro () + +# install_data ( files/directories [INTO destination] ) +# This installs additional data files or directories. +# USE: install_data ( extra data.dat ) +# USE: install_data ( image1.png image2.png INTO images ) +# For directories, supports optional PATTERN/REGEX arguments like install(). +set ( CPACK_COMPONENT_DATA_DISPLAY_NAME "${DIST_NAME} Data" ) +set ( CPACK_COMPONENT_DATA_DESCRIPTION + "Application data. Installed into ${INSTALL_DATA}." ) +macro ( install_data ) + parse_arguments ( _ARG "INTO;PATTERN;REGEX" "" ${ARGN} ) + _complete_install_args() + foreach ( _file ${_ARG_DEFAULT_ARGS} ) + if ( IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${_file}" ) + install ( DIRECTORY ${_file} + DESTINATION ${INSTALL_DATA}/${_ARG_INTO} + COMPONENT Data ${_ARG_PATTERN} ${_ARG_REGEX} ) + else () + install ( FILES ${_file} DESTINATION ${INSTALL_DATA}/${_ARG_INTO} + COMPONENT Data ) + endif () + endforeach() +endmacro () + +# INSTALL_DOC ( files/directories [INTO destination] ) +# This installs documentation content +# USE: install_doc ( doc/ doc.pdf ) +# USE: install_doc ( index.html INTO html ) +# For directories, supports optional PATTERN/REGEX arguments like install(). +set ( CPACK_COMPONENT_DOCUMENTATION_DISPLAY_NAME "${DIST_NAME} Documentation" ) +set ( CPACK_COMPONENT_DOCUMENTATION_DESCRIPTION + "Application documentation. Installed into ${INSTALL_DOC}." ) +macro ( install_doc ) + parse_arguments ( _ARG "INTO;PATTERN;REGEX" "" ${ARGN} ) + _complete_install_args() + foreach ( _file ${_ARG_DEFAULT_ARGS} ) + if ( IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${_file}" ) + install ( DIRECTORY ${_file} DESTINATION ${INSTALL_DOC}/${_ARG_INTO} + COMPONENT Documentation ${_ARG_PATTERN} ${_ARG_REGEX} ) + else () + install ( FILES ${_file} DESTINATION ${INSTALL_DOC}/${_ARG_INTO} + COMPONENT Documentation ) + endif () + endforeach() +endmacro () + +# install_example ( files/directories [INTO destination] ) +# This installs additional examples +# USE: install_example ( examples/ exampleA ) +# USE: install_example ( super_example super_data INTO super) +# For directories, supports optional PATTERN/REGEX argument like install(). +set ( CPACK_COMPONENT_EXAMPLE_DISPLAY_NAME "${DIST_NAME} Examples" ) +set ( CPACK_COMPONENT_EXAMPLE_DESCRIPTION + "Examples and their associated data. Installed into ${INSTALL_EXAMPLE}." ) +macro ( install_example ) + parse_arguments ( _ARG "INTO;PATTERN;REGEX" "" ${ARGN} ) + _complete_install_args() + foreach ( _file ${_ARG_DEFAULT_ARGS} ) + if ( IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${_file}" ) + install ( DIRECTORY ${_file} DESTINATION ${INSTALL_EXAMPLE}/${_ARG_INTO} + COMPONENT Example ${_ARG_PATTERN} ${_ARG_REGEX} ) + else () + install ( FILES ${_file} DESTINATION ${INSTALL_EXAMPLE}/${_ARG_INTO} + COMPONENT Example ) + endif () + endforeach() +endmacro () + +# install_test ( files/directories [INTO destination] ) +# This installs tests and test files, DOES NOT EXECUTE TESTS +# USE: install_test ( my_test data.sql ) +# USE: install_test ( feature_x_test INTO x ) +# For directories, supports optional PATTERN/REGEX argument like install(). +set ( CPACK_COMPONENT_TEST_DISPLAY_NAME "${DIST_NAME} Tests" ) +set ( CPACK_COMPONENT_TEST_DESCRIPTION + "Tests and associated data. Installed into ${INSTALL_TEST}." ) +macro ( install_test ) + parse_arguments ( _ARG "INTO;PATTERN;REGEX" "" ${ARGN} ) + _complete_install_args() + foreach ( _file ${_ARG_DEFAULT_ARGS} ) + if ( IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${_file}" ) + install ( DIRECTORY ${_file} DESTINATION ${INSTALL_TEST}/${_ARG_INTO} + COMPONENT Test ${_ARG_PATTERN} ${_ARG_REGEX} ) + else () + install ( FILES ${_file} DESTINATION ${INSTALL_TEST}/${_ARG_INTO} + COMPONENT Test ) + endif () + endforeach() +endmacro () + +# install_foo ( files/directories [INTO destination] ) +# This installs optional or otherwise unneeded content +# USE: install_foo ( etc/ example.doc ) +# USE: install_foo ( icon.png logo.png INTO icons) +# For directories, supports optional PATTERN/REGEX argument like install(). +set ( CPACK_COMPONENT_OTHER_DISPLAY_NAME "${DIST_NAME} Unspecified Content" ) +set ( CPACK_COMPONENT_OTHER_DESCRIPTION + "Other unspecified content. Installed into ${INSTALL_FOO}." ) +macro ( install_foo ) + parse_arguments ( _ARG "INTO;PATTERN;REGEX" "" ${ARGN} ) + _complete_install_args() + foreach ( _file ${_ARG_DEFAULT_ARGS} ) + if ( IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${_file}" ) + install ( DIRECTORY ${_file} DESTINATION ${INSTALL_FOO}/${_ARG_INTO} + COMPONENT Other ${_ARG_PATTERN} ${_ARG_REGEX} ) + else () + install ( FILES ${_file} DESTINATION ${INSTALL_FOO}/${_ARG_INTO} + COMPONENT Other ) + endif () + endforeach() +endmacro () + +## CTest defaults + +## CPack defaults +set ( CPACK_GENERATOR "ZIP" ) +set ( CPACK_STRIP_FILES TRUE ) +set ( CPACK_PACKAGE_NAME "${DIST_NAME}" ) +set ( CPACK_PACKAGE_VERSION "${DIST_VERSION}") +set ( CPACK_PACKAGE_VENDOR "LuaDist" ) +set ( CPACK_COMPONENTS_ALL Runtime Library Header Data Documentation Example Other ) +include ( CPack ) diff --git a/cmake/lua.cmake b/cmake/lua.cmake new file mode 100644 index 0000000..80bbc5f --- /dev/null +++ b/cmake/lua.cmake @@ -0,0 +1,293 @@ +# LuaDist CMake utility library for Lua. +# +# Copyright (C) 2007-2012 LuaDist. +# by David Manura, Peter Drahos +# Redistribution and use of this file is allowed according to the terms of the MIT license. +# For details see the COPYRIGHT file distributed with LuaDist. +# Please note that the package source code is licensed under its own license. + +set ( INSTALL_LMOD ${INSTALL_LIB}/lua + CACHE PATH "Directory to install Lua modules." ) +set ( INSTALL_CMOD ${INSTALL_LIB}/lua + CACHE PATH "Directory to install Lua binary modules." ) + +option ( SKIP_LUA_WRAPPER + "Do not build and install Lua executable wrappers." OFF) + +# List of (Lua module name, file path) pairs. +# Used internally by add_lua_test. Built by add_lua_module. +set ( _lua_modules ) + +# utility function: appends path `path` to path `basepath`, properly +# handling cases when `path` may be relative or absolute. +macro ( _append_path basepath path result ) + if ( IS_ABSOLUTE "${path}" ) + set ( ${result} "${path}" ) + else () + set ( ${result} "${basepath}/${path}" ) + endif () +endmacro () + +# install_lua_executable ( target source ) +# Automatically generate a binary if srlua package is available +# The application or its source will be placed into /bin +# If the application source did not have .lua suffix then it will be added +# USE: lua_executable ( sputnik src/sputnik.lua ) +macro ( install_lua_executable _name _source ) + get_filename_component ( _source_name ${_source} NAME_WE ) + # Find srlua and glue + find_program( SRLUA_EXECUTABLE NAMES srlua ) + find_program( GLUE_EXECUTABLE NAMES glue ) + # Executable output + set ( _exe ${CMAKE_CURRENT_BINARY_DIR}/${_name}${CMAKE_EXECUTABLE_SUFFIX} ) + if ( NOT SKIP_LUA_WRAPPER AND SRLUA_EXECUTABLE AND GLUE_EXECUTABLE ) + # Generate binary gluing the lua code to srlua, this is a robuust approach for most systems + add_custom_command( + OUTPUT ${_exe} + COMMAND ${GLUE_EXECUTABLE} + ARGS ${SRLUA_EXECUTABLE} ${_source} ${_exe} + DEPENDS ${_source} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + VERBATIM + ) + # Make sure we have a target associated with the binary + add_custom_target(${_name} ALL + DEPENDS ${_exe} + ) + # Install with run permissions + install ( PROGRAMS ${_exe} DESTINATION ${INSTALL_BIN} COMPONENT Runtime) + # Also install source as optional resurce + install ( FILES ${_source} DESTINATION ${INSTALL_FOO} COMPONENT Other ) + else() + # Install into bin as is but without the lua suffix, we assume the executable uses UNIX shebang/hash-bang magic + install ( PROGRAMS ${_source} DESTINATION ${INSTALL_BIN} + RENAME ${_source_name} + COMPONENT Runtime + ) + endif() +endmacro () + +macro ( _lua_module_helper is_install _name ) + parse_arguments ( _MODULE "LINK;ALL_IN_ONE" "" ${ARGN} ) + # _target is CMake-compatible target name for module (e.g. socket_core). + # _module is relative path of target (e.g. socket/core), + # without extension (e.g. .lua/.so/.dll). + # _MODULE_SRC is list of module source files (e.g. .lua and .c files). + # _MODULE_NAMES is list of module names (e.g. socket.core). + if ( _MODULE_ALL_IN_ONE ) + string ( REGEX REPLACE "\\..*" "" _target "${_name}" ) + string ( REGEX REPLACE "\\..*" "" _module "${_name}" ) + set ( _target "${_target}_all_in_one") + set ( _MODULE_SRC ${_MODULE_ALL_IN_ONE} ) + set ( _MODULE_NAMES ${_name} ${_MODULE_DEFAULT_ARGS} ) + else () + string ( REPLACE "." "_" _target "${_name}" ) + string ( REPLACE "." "/" _module "${_name}" ) + set ( _MODULE_SRC ${_MODULE_DEFAULT_ARGS} ) + set ( _MODULE_NAMES ${_name} ) + endif () + if ( NOT _MODULE_SRC ) + message ( FATAL_ERROR "no module sources specified" ) + endif () + list ( GET _MODULE_SRC 0 _first_source ) + + get_filename_component ( _ext ${_first_source} EXT ) + if ( _ext STREQUAL ".lua" ) # Lua source module + list ( LENGTH _MODULE_SRC _len ) + if ( _len GREATER 1 ) + message ( FATAL_ERROR "more than one source file specified" ) + endif () + + set ( _module "${_module}.lua" ) + + get_filename_component ( _module_dir ${_module} PATH ) + get_filename_component ( _module_filename ${_module} NAME ) + _append_path ( "${CMAKE_CURRENT_SOURCE_DIR}" "${_first_source}" _module_path ) + list ( APPEND _lua_modules "${_name}" "${_module_path}" ) + + if ( ${is_install} ) + install ( FILES ${_first_source} DESTINATION ${INSTALL_LMOD}/${_module_dir} + RENAME ${_module_filename} + COMPONENT Runtime + ) + endif () + else () # Lua C binary module + enable_language ( C ) + find_package ( Lua REQUIRED ) + include_directories ( ${LUA_INCLUDE_DIR} ) + + set ( _module "${_module}${CMAKE_SHARED_MODULE_SUFFIX}" ) + + get_filename_component ( _module_dir ${_module} PATH ) + get_filename_component ( _module_filenamebase ${_module} NAME_WE ) + foreach ( _thisname ${_MODULE_NAMES} ) + list ( APPEND _lua_modules "${_thisname}" + "${CMAKE_CURRENT_BINARY_DIR}/\${CMAKE_CFG_INTDIR}/${_module}" ) + endforeach () + + add_library( ${_target} MODULE ${_MODULE_SRC}) + target_link_libraries ( ${_target} ${LUA_LIBRARY} ${_MODULE_LINK} ) + set_target_properties ( ${_target} PROPERTIES LIBRARY_OUTPUT_DIRECTORY + "${_module_dir}" PREFIX "" OUTPUT_NAME "${_module_filenamebase}" ) + if ( ${is_install} ) + install ( TARGETS ${_target} DESTINATION ${INSTALL_CMOD}/${_module_dir} COMPONENT Runtime) + endif () + endif () +endmacro () + +# add_lua_module +# Builds a Lua source module into a destination locatable by Lua +# require syntax. +# Binary modules are also supported where this function takes sources and +# libraries to compile separated by LINK keyword. +# USE: add_lua_module ( socket.http src/http.lua ) +# USE2: add_lua_module ( mime.core src/mime.c ) +# USE3: add_lua_module ( socket.core ${SRC_SOCKET} LINK ${LIB_SOCKET} ) +# USE4: add_lua_module ( ssl.context ssl.core ALL_IN_ONE src/context.c src/ssl.c ) +# This form builds an "all-in-one" module (e.g. ssl.so or ssl.dll containing +# both modules ssl.context and ssl.core). The CMake target name will be +# ssl_all_in_one. +# Also sets variable _module_path (relative path where module typically +# would be installed). +macro ( add_lua_module ) + _lua_module_helper ( 0 ${ARGN} ) +endmacro () + + +# install_lua_module +# This is the same as `add_lua_module` but also installs the module. +# USE: install_lua_module ( socket.http src/http.lua ) +# USE2: install_lua_module ( mime.core src/mime.c ) +# USE3: install_lua_module ( socket.core ${SRC_SOCKET} LINK ${LIB_SOCKET} ) +macro ( install_lua_module ) + _lua_module_helper ( 1 ${ARGN} ) +endmacro () + +# Builds string representing Lua table mapping Lua modules names to file +# paths. Used internally. +macro ( _make_module_table _outvar ) + set ( ${_outvar} ) + list ( LENGTH _lua_modules _n ) + if ( ${_n} GREATER 0 ) # avoids cmake complaint + foreach ( _i RANGE 1 ${_n} 2 ) + list ( GET _lua_modules ${_i} _path ) + math ( EXPR _ii ${_i}-1 ) + list ( GET _lua_modules ${_ii} _name ) + set ( ${_outvar} "${_table} ['${_name}'] = '${_path}'\;\n") + endforeach () + endif () + set ( ${_outvar} +"local modules = { +${_table}}" ) +endmacro () + +# add_lua_test ( _testfile [ WORKING_DIRECTORY _working_dir ] ) +# Runs Lua script `_testfile` under CTest tester. +# Optional named argument `WORKING_DIRECTORY` is current working directory to +# run test under (defaults to ${CMAKE_CURRENT_BINARY_DIR}). +# Both paths, if relative, are relative to ${CMAKE_CURRENT_SOURCE_DIR}. +# Any modules previously defined with install_lua_module are automatically +# preloaded (via package.preload) prior to running the test script. +# Under LuaDist, set test=true in config.lua to enable testing. +# USE: add_lua_test ( test/test1.lua [args...] [WORKING_DIRECTORY dir]) +macro ( add_lua_test _testfile ) + if ( NOT SKIP_TESTING ) + parse_arguments ( _ARG "WORKING_DIRECTORY" "" ${ARGN} ) + include ( CTest ) + find_program ( LUA NAMES lua lua.bat ) + get_filename_component ( TESTFILEABS ${_testfile} ABSOLUTE ) + get_filename_component ( TESTFILENAME ${_testfile} NAME ) + get_filename_component ( TESTFILEBASE ${_testfile} NAME_WE ) + + # Write wrapper script. + # Note: One simple way to allow the script to find modules is + # to just put them in package.preload. + set ( TESTWRAPPER ${CMAKE_CURRENT_BINARY_DIR}/${TESTFILENAME} ) + _make_module_table ( _table ) + set ( TESTWRAPPERSOURCE +"local CMAKE_CFG_INTDIR = ... or '.' +${_table} +local function preload_modules(modules) + for name, path in pairs(modules) do + if path:match'%.lua' then + package.preload[name] = assert(loadfile(path)) + else + local name = name:gsub('.*%-', '') -- remove any hyphen prefix + local symbol = 'luaopen_' .. name:gsub('%.', '_') + --improve: generalize to support all-in-one loader? + local path = path:gsub('%$%{CMAKE_CFG_INTDIR%}', CMAKE_CFG_INTDIR) + package.preload[name] = assert(package.loadlib(path, symbol)) + end + end +end +preload_modules(modules) +arg[0] = '${TESTFILEABS}' +table.remove(arg, 1) +return assert(loadfile '${TESTFILEABS}')(unpack(arg)) +" ) + if ( _ARG_WORKING_DIRECTORY ) + get_filename_component ( + TESTCURRENTDIRABS ${_ARG_WORKING_DIRECTORY} ABSOLUTE ) + # note: CMake 2.6 (unlike 2.8) lacks WORKING_DIRECTORY parameter. + set ( _pre ${CMAKE_COMMAND} -E chdir "${TESTCURRENTDIRABS}" ) + endif () + file ( WRITE ${TESTWRAPPER} ${TESTWRAPPERSOURCE}) + add_test ( NAME ${TESTFILEBASE} COMMAND ${_pre} ${LUA} + ${TESTWRAPPER} "${CMAKE_CFG_INTDIR}" + ${_ARG_DEFAULT_ARGS} ) + endif () + # see also http://gdcm.svn.sourceforge.net/viewvc/gdcm/Sandbox/CMakeModules/UsePythonTest.cmake + # Note: ${CMAKE_CFG_INTDIR} is a command-line argument to allow proper + # expansion by the native build tool. +endmacro () + + +# Converts Lua source file `_source` to binary string embedded in C source +# file `_target`. Optionally compiles Lua source to byte code (not available +# under LuaJIT2, which doesn't have a bytecode loader). Additionally, Lua +# versions of bin2c [1] and luac [2] may be passed respectively as additional +# arguments. +# +# [1] http://lua-users.org/wiki/BinToCee +# [2] http://lua-users.org/wiki/LuaCompilerInLua +function ( add_lua_bin2c _target _source ) + find_program ( LUA NAMES lua lua.bat ) + execute_process ( COMMAND ${LUA} -e "string.dump(function()end)" + RESULT_VARIABLE _LUA_DUMP_RESULT ERROR_QUIET ) + if ( NOT ${_LUA_DUMP_RESULT} ) + SET ( HAVE_LUA_DUMP true ) + endif () + message ( "-- string.dump=${HAVE_LUA_DUMP}" ) + + if ( ARGV2 ) + get_filename_component ( BIN2C ${ARGV2} ABSOLUTE ) + set ( BIN2C ${LUA} ${BIN2C} ) + else () + find_program ( BIN2C NAMES bin2c bin2c.bat ) + endif () + if ( HAVE_LUA_DUMP ) + if ( ARGV3 ) + get_filename_component ( LUAC ${ARGV3} ABSOLUTE ) + set ( LUAC ${LUA} ${LUAC} ) + else () + find_program ( LUAC NAMES luac luac.bat ) + endif () + endif ( HAVE_LUA_DUMP ) + message ( "-- bin2c=${BIN2C}" ) + message ( "-- luac=${LUAC}" ) + + get_filename_component ( SOURCEABS ${_source} ABSOLUTE ) + if ( HAVE_LUA_DUMP ) + get_filename_component ( SOURCEBASE ${_source} NAME_WE ) + add_custom_command ( + OUTPUT ${_target} DEPENDS ${_source} + COMMAND ${LUAC} -o ${CMAKE_CURRENT_BINARY_DIR}/${SOURCEBASE}.lo + ${SOURCEABS} + COMMAND ${BIN2C} ${CMAKE_CURRENT_BINARY_DIR}/${SOURCEBASE}.lo + ">${_target}" ) + else () + add_custom_command ( + OUTPUT ${_target} DEPENDS ${SOURCEABS} + COMMAND ${BIN2C} ${_source} ">${_target}" ) + endif () +endfunction() diff --git a/dist.info b/dist.info new file mode 100644 index 0000000..f797f89 --- /dev/null +++ b/dist.info @@ -0,0 +1,14 @@ +--- This file is part of LuaDist project + +name = "lemock" +version = "0.6" + +desc = "Mock creation module intended for use together with a unit test framework such as lunit or lunity." +author = "Tommy Petterson" +license = "MIT" +url = "http://lemock.luaforge.net" +maintainer = "Peter Drahos" + +depends = { + "lua ~> 5.1" +} \ No newline at end of file diff --git a/src/action/call.nw b/src/action/call.nw new file mode 100644 index 0000000..52217a9 --- /dev/null +++ b/src/action/call.nw @@ -0,0 +1,122 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +The Call Action +############### + +The index action returns a Callable object, which will catch calls. The +catching of the call will record a call action, and modify the index action +to record the fact that it should return a Callable object during replay. + +<>= + function call_test () + m.foo(1,2,3) + mc:replay() + local tmp = m.foo(1,2,3) + assert_nil( tmp ) + mc:verify() + end + function call_anyarg_test () + m.foo(1,mc.ANYARG,3) + mc:replay() + local tmp = m.foo(1,2,3) + mc:verify() + end + function call_anyargs_test () + m.foo(mc.ANYARGS) + mc:replay() + local tmp = m.foo(1,2,3) + mc:verify() + end + function call_anyargs_bad_fails_test () + local ok, err = pcall( function() m.foo(mc.ANYARGS, 1) end ) + assert_false( ok, "ANYARGS misused" ) + assert_match( "ANYARGS not at end", err ) + end + function call_return_test () + m.foo(1,2,3) ;mc:returns( 0, 9 ) + mc:replay() + local tmp1, tmp2 = m.foo(1,2,3) + assert_equal( 0, tmp1 ) + assert_equal( 9, tmp2 ) + mc:verify() + end + function call_wrong_name_fails_test () + m.foo(1,2,3) ;mc:returns( 0 ) + mc:replay() + local ok, err = pcall( function() m:bar(1,2,3) end ) + assert_false( ok, "replay wrong index" ) + assert_match( "Unexpected action index bar", err ) + end + function call_wrong_arg_fails_test () + m.foo(1,2,3) ;mc:returns( 0 ) + mc:replay() + local ok, err = pcall( function() m.foo(1) end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action call foo", err ) + end + function call_throws_error_test () + m.boo('Ba') ;mc:error( "Call throws error" ) + mc:replay() + local ok, err = pcall( function() m.boo('Ba') end ) + assert_false( ok, "did not throw error" ) + assert_match( "Call throws error", err ) + end +@ + +Record Phase +============ + +<>= + function Callable.record:__call (...) + local index_action = self.action + local m = index_action.mock + local mc = mock_controller_map[m] + assert( mc.is_recording, "client uses cached callable from recording" ) + mc:make_callable( index_action ) + mc:add_action( Action.call:new( m, index_action.key, ... )) + end + +<>= + function Action.call:new (m, key, ...) + local a = Action.generic_call.new( self, m, ... ) + a.key = key + return a + end +@ + +Replay Phase +============ + +<>= + function Callable.replay:__call (...) + local index_action = self.action + local m = index_action.mock + local mc = mock_controller_map[m] + local call_action = mc:lookup( Action.call:new( m, index_action.key, ... )) + mc:replay_action( call_action ) + if call_action.throws_error then + error( call_action.errorvalue, 2 ) + end + return call_action:get_returnvalue() + end + +<>= + function call_match_test () + local m = {} + local a = Action.call:new( m, 'foo', 4, 'bb' ) + assert_true( a:match( Action.call:new( m, 'foo', 4, 'bb' ))) + assert_false( a:match( Action.call:new( {}, 'foo', 4, 'bb' ))) + assert_false( a:match( Action.call:new( m, 'bar', 4, 'bb' ))) + assert_false( a:match( Action.call:new( m, 'foo', 1, 'bb' ))) + assert_false( a:match( Action.call:new( m, 'foo', 4, 'b' ))) + assert_false( a:match( Action.call:new( m, 'foo', 4, 'bb', 'cc' ))) + end + +<>= + function Action.call:match (q) + if not Action.generic_call.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true + end diff --git a/src/action/generic_call.nw b/src/action/generic_call.nw new file mode 100644 index 0000000..491fc79 --- /dev/null +++ b/src/action/generic_call.nw @@ -0,0 +1,80 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +Class Action.generic_call +######################### + +The generic_call action class implements the common tasks of the two +different call action types. + +There are two types of calls in Lua: a call of a function, and a call of +the [[__call]] meta method for something else (e.g., a table). It is +normally not necessary to differentiate the two types in the mock, because +only the behavior is simulated so it does not matter what the simulated +object would return. The mock always returns a Callable, which records a +call action. The exception is when the mock object itself is called, which +records a selfcall action. + +Note: a special case is if the mock contains another mock that can be +called. This is awkward, but possible, to simulate by first referencing the +inner mock object from the outer (an index action), and then recording the +inner mock object as the returned value of that index action, and finally +calling the second mock object. + + +new +--- + +This method is extended by the concrete Action classes. + +<>= + function Action.generic_call:new (m, ...) + local a = Action.generic.new( self, m ) + a.argv = Argv:new(...) + return a + end +@ + +match +----- + +This method is extended by the concrete Action classes. + +<>= + function Action.generic_call:match (q) + if not Action.generic.match( self, q ) then return false end + if not self.argv:equal( q.argv ) then return false end + return true + end +@ + +set_returnvalue +--------------- + +<>= + function Action.generic_call:set_returnvalue (...) + self.returnvalue = Argv:new(...) + self.has_returnvalue = true + end +@ + +get_returnvalue +--------------- + +<>= + function generic_call_set_and_get_returnvalue_test () + local a = Action.generic_call:new() + assert_equal( 0, select('#', a:get_returnvalue() )) + a:set_returnvalue( nil, false ) + local r1, r2 = a:get_returnvalue() + assert_equal( nil, r1 ) + assert_equal( false, r2 ) + end + +<>= + function Action.generic_call:get_returnvalue () + if self.has_returnvalue then + return self.returnvalue:unpack() + end + end diff --git a/src/action/index.nw b/src/action/index.nw new file mode 100644 index 0000000..80869ff --- /dev/null +++ b/src/action/index.nw @@ -0,0 +1,136 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +The Index Action +################ + +The index action can either be "called" (via the returned Callable) or be +given a return value, or neither in which case it will return nil during +replay. + +<>= + function index_test () + local tmp = m.foo + mc:replay() + local tmp = m.foo + assert_nil( tmp ) + mc:verify() + end + function index_returns_test () + local tmp = m.foo ;mc:returns( 1 ) + mc:replay() + local tmp = m.foo + assert_equal( 1, tmp ) + mc:verify() + end + function index_wrong_key_fails_test () + local tmp = m.foo ;mc:returns( 1 ) + mc:replay() + local ok, err = pcall( function() local tmp = m.bar end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action index bar", err ) + end + function index_throws_error_test () + local tmp = m.foo ;mc:error( "Index throws error" ) + mc:replay() + local ok, err = pcall( function() tmp = m.foo end ) + assert_false( ok, "did not throw error" ) + assert_match( "Index throws error", err ) + end +@ + +Record Phase +============ + +<>= + function Mock.record:__index (key) + local mc = mock_controller_map[self] + local action = Action.index:new( self, key ) + mc:add_action( action ) + return Callable.record:new( action ) + end +@ + +Action new +---------- + +<>= + function create_index_action_test () + local m = {} + local a = Action.index:new( m, 'foo' ) + assert_equal( m, a.mock ) + assert_equal( 'foo', a.key ) + end + +<>= + function Action.index:new (m, key) + local a = Action.generic.new( self, m ) + a.key = key + return a + end +@ + +Action set_returnvalue +---------------------- + +<>= + function index_returnvalue_test () + local a = Action.index:new( {}, -3 ) + a:set_returnvalue( 'foo' ) + assert_equal( 'foo', a:get_returnvalue() ) + end + +<>= + function Action.index:set_returnvalue (v) + self.returnvalue = v + self.has_returnvalue = true + end +@ + +Replay Phase +============ + +<>= + function Mock.replay:__index (key) + local mc = mock_controller_map[self] + local index_action = mc:lookup( Action.index:new( self, key )) + mc:replay_action( index_action ) + if index_action.throws_error then + error( index_action.errorvalue, 2 ) + end + if index_action.is_callable then + return Callable.replay:new( index_action ) + else + return index_action:get_returnvalue() + end + end +@ + +Action match +------------ + +<>= + function index_match_test () + local m = {} + local a = Action.index:new( m, -1 ) + assert_true( a:match( Action.index:new( m, -1 ))) + assert_false( a:match( Action.index:new( {}, -1 ))) + assert_false( a:match( Action.index:new( m, 'a' ))) + end + +<>= + function Action.index:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + return true + end +@ + +Action get_returnvalue +---------------------- + +<>= + function Action.index:get_returnvalue () + return self.returnvalue + end diff --git a/src/action/newindex.nw b/src/action/newindex.nw new file mode 100644 index 0000000..b54cf2a --- /dev/null +++ b/src/action/newindex.nw @@ -0,0 +1,109 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +The Newindex Action +################### + +<>= + function newindex_test () + m.foo = 1 + mc:replay() + m.foo = 1 + mc:verify() + end + function newindex_anyarg_test () + m.foo = mc.ANYARG + mc:replay() + m.foo = 1 + mc:verify() + end + function newindex_wrong_key_fails_test () + m.foo = 1 + mc:replay() + local ok, err = pcall( function() m.bar = 1 end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action newindex", err ) + end + function newindex_wrong_value_fails_test () + m.foo = 1 + mc:replay() + local ok, err = pcall( function() m.foo = 0 end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action newindex foo", err ) + end + function newindex_throws_error_test () + m.foo = 1 ;mc:error( "newindex throws error" ) + mc:replay() + local ok, err = pcall( function() m.foo = 1 end ) + assert_false( ok, "did not throw error" ) + assert_match( "newindex throws error", err ) + end +@ + +Record Phase +============ + +<>= + function Mock.record:__newindex (key, val) + local mc = mock_controller_map[self] + mc:add_action( Action.newindex:new( self, key, val )) + end + +<>= + function Action.newindex:new (m, key, val) + local a = Action.generic.new( self, m ) + a.key = key + a.val = val + return a + end +@ + +Replay Phase +============ + +<>= + function Mock.replay:__newindex (key, val) + local mc = mock_controller_map[self] + local newindex_action = mc:lookup( Action.newindex:new( self, key, val )) + mc:replay_action( newindex_action ) + if newindex_action.throws_error then + error( newindex_action.errorvalue, 2 ) + end + end + +<>= + function newindex_match_test () + local m = {} + local a = Action.newindex:new( m, 'foo', 17 ) + assert_true( a:match( Action.newindex:new( m, 'foo', 17 ))) + assert_false( a:match( Action.newindex:new( {}, 'foo', 17 ))) + assert_false( a:match( Action.newindex:new( m, 'fo', 17 ))) + assert_false( a:match( Action.newindex:new( m, 'foo', 7 ))) + end + function newindex_anyarg_test () + local m = {} + local a = Action.newindex:new( m, 'foo', Argv.ANYARG ) + local b = Action.newindex:new( m, 'foo', 33 ) + local c = Action.newindex:new( m, 'foo', nil ) + assert_true( a:match(b) ) + assert_true( b:match(a) ) + assert_true( a:match(c) ) + assert_true( c:match(a) ) + end + function newindex_NaN_test () + local m = {} + local nan = 0/0 + local a = Action.newindex:new( m, m, nan ) + assert_true( a:match( Action.newindex:new( m, m, nan ))) + end + +<>= + function Action.newindex:match (q) + if not Action.generic.match( self, q ) then return false end + if self.key ~= q.key then return false end + if not value_equal( self.val, q.val ) + and self.val ~= Argv.ANYARG + and q.val ~= Argv.ANYARG then return false end + return true + end diff --git a/src/action/selfcall.nw b/src/action/selfcall.nw new file mode 100644 index 0000000..91fe6d1 --- /dev/null +++ b/src/action/selfcall.nw @@ -0,0 +1,95 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +The Call Action +############### + +The selfcall action is made to the Mock object itself, so no Callable or +index action is needed. + +<>= + function selfcall_test () + m(11) + mc:replay() + local tmp = m(11) + assert_nil( tmp ) + mc:verify() + end + function selfcall_returns_test () + m(99) ;mc:returns(1,nil,'foo') + mc:replay() + local a,b,c = m(99) + assert_equal( 1, a ) + assert_equal( nil, b ) + assert_equal( 'foo', c ) + mc:verify() + end + function selfcall_wrong_argument_fails_test () + m(99) ;mc:returns('a','b','c') + mc:replay() + local ok, err = pcall( function() m(90) end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action selfcall", err ) + end + function selfcall_wrong_number_of_arguments_fails_test () + m(1,2,3) + mc:replay() + local ok, err = pcall( function() m(1,2,3,4) end ) + assert_false( ok, "replay succeeded" ) + assert_match( "Unexpected action selfcall", err ) + end + function selfcall_throws_error_test () + m('Ba') ;mc:error( "Selfcall throws error" ) + mc:replay() + local ok, err = pcall( function() m('Ba') end ) + assert_false( ok, "did not throw error" ) + assert_match( "Selfcall throws error", err ) + end +@ + +Record Phase +============ + +<>= + function Mock.record:__call (...) + local mc = mock_controller_map[self] + mc:add_action( Action.selfcall:new( self, ... )) + end + +<>= + function Action.selfcall:new (m, ...) + local a = Action.generic_call.new( self, m, ... ) + return a + end +@ + +Replay Phase +============ + +<>= + function Mock.replay:__call (...) + local mc = mock_controller_map[self] + local selfcall_action = mc:lookup( Action.selfcall:new( self, ... )) + mc:replay_action( selfcall_action ) + if selfcall_action.throws_error then + error( selfcall_action.errorvalue, 2 ) + end + return selfcall_action:get_returnvalue() + end + +<>= + function selfcall_match_test () + local m = {} + local a = Action.selfcall:new( m, 5, nil, false ) + assert_true( a:match( Action.selfcall:new( m, 5, nil, false ))) + assert_false( a:match( Action.selfcall:new( {}, 5, nil, false ))) + assert_false( a:match( Action.selfcall:new( m, nil, nil, false ))) + assert_false( a:match( Action.selfcall:new( m, 5, false, false ))) + assert_false( a:match( Action.selfcall:new( m, 5, nil, nil ))) + end + +<>= + function Action.selfcall:match (q) + return Action.generic_call.match( self, q ) + end diff --git a/src/argv.nw b/src/argv.nw new file mode 100644 index 0000000..17fe98d --- /dev/null +++ b/src/argv.nw @@ -0,0 +1,158 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +Class Argv +########## + +It is convenient to handle argument lists and return lists as an abstract +type. + +<>= + +An //anyarg// is a special argument used when recording, that will match +any argument during replay. It can appear anywhere and any times in an +argument list, or as the argument in an assignment, to replace real +arguments. There is also //anyargs//, which will match any number +(including zero) of any arguments. Anyargs can only appear as the last +argument of an argument list. Anyarg and anyargs are handy when the actual +values of the arguments during replay are unimportant or unknown. +@ + +new +--- + +<>= + function new_test () + Argv:new( Argv.ANYARGS ) + Argv:new( 1, Argv.ANYARGS ) + Argv:new( 1, 2, Argv.ANYARGS ) + end + function new_anyargs_with_extra_arguments_fails_test () + local l = {} + l['ANYARGS,1'] = { Argv.ANYARGS, 1 } + l['ANYARGS,ANYARGS' ] = { Argv.ANYARGS, Argv.ANYARGS } + l['1,ANYARGS,1'] = { 1, Argv.ANYARGS, 1 } + l['1,ANYARGS,ANYARGS'] = { 1, Argv.ANYARGS, Argv.ANYARGS } + for msg, args in pairs( l ) do + local ok, err = pcall( function() Argv:new( unpack(args) ) end ) + assert_false( ok, "Bad ANYARGS accepted for "..msg ) + assert_match( "ANYARGS not at end", err ) + end + end + +<>= + function Argv:new (...) + local av = object( self ) + av.v = {...} + av.len = select('#',...) + for i = 1, av.len - 1 do + if av.v[i] == Argv.ANYARGS then + error( "ANYARGS not at end.", 0 ) + end + end + return av + end +@ + +equal +----- + +<>= + local l = {} + local function p (...) l[#l+1] = { n=select('#',...), ... } end + p() p(nil) p(nil,nil) p(false) p({}) p(false,nil,{},nil) p(nil,p) + p(true) p(0.1,'','a') p(1/0,nil,0/0) p(0/0) p(0/0, true) p(0/0, false) + function equal_test () + local a1, a2, f, op + for i = 1, #l do + ai = Argv:new( unpack( l[i], 1, l[i].n )) + for j = 1, #l do + aj = Argv:new( unpack( l[j], 1, l[j].n )) + if i == j then + f, op = assert_true, ') ~= (' + else + f, op = assert_false, ') == (' + end + f( ai:equal(aj), '('..ai:tostring()..op..aj:tostring()..')' ) + end + end + end + function equal_anyargs_test () + local a, b = {}, {} + a[1] = Argv:new( Argv.ANYARGS ) + a[2] = Argv:new( 6, Argv.ANYARGS ) + a[3] = Argv:new( 6, 5, Argv.ANYARGS ) + for i = 1, #l do + b[1] = Argv:new( unpack( l[i], 1, l[i].n )) + b[2] = Argv:new( 6, unpack( l[i], 1, l[i].n )) + b[3] = Argv:new( 6, 5, unpack( l[i], 1, l[i].n )) + for j = 1, 3 do + local astr = '('..a[j]:tostring()..')' + local bstr = '('..b[j]:tostring()..')' + assert_true( a[j]:equal(b[j]), astr..' ~= '..bstr ) + assert_true( b[j]:equal(a[j]), bstr..' ~= '..astr ) + end + end + end + function equal_anyarg_test () + local l = { 1, 2, 3, 4, 5, 6, 7, 8, 9 } + local a1 = Argv:new( unpack(l) ) + for i = 1, 9 do + l[i] = Argv.ANYARG + local a2 = Argv:new( unpack(l) ) + assert_true( a1:equal(a2) ) + assert_true( a2:equal(a1) ) + l[i] = i + end + end +@ +The comparison with ANYARGS is a bit tricky, because it must match the +empty list. The list //excluding// the final ANYARGS argument (its length +minus one) must match the other list. + +ANYARG and ANYARGS are created as unique identifiers, with local aliases +for speed. + +<>= + Argv.ANYARGS = newproxy() local ANYARGS = Argv.ANYARGS + Argv.ANYARG = newproxy() local ANYARG = Argv.ANYARG + function Argv:equal (other) + local a1, n1 = self.v, self.len + local a2, n2 = other.v, other.len + if n1-1 <= n2 and a1[n1] == ANYARGS then + n1 = n1-1 + n2 = n1 + elseif n2-1 <= n1 and a2[n2] == ANYARGS then + n2 = n2-1 + n1 = n2 + end + if n1 ~= n2 then + return false + end + for i = 1, n1 do + local v1, v2 = a1[i], a2[i] + if not value_equal(v1,v2) and v1 ~= ANYARG and v2 ~= ANYARG then + return false + end + end + return true + end +@ + +unpack +------ + +<>= + function unpack_test () + local a, b, c = Argv:new( false, nil, 7 ):unpack() + assert_equal( false, a ) + assert_equal( nil, b ) + assert_equal( 7, c ) + end + +<>= + function Argv:unpack () + return unpack( self.v, 1, self.len ) + end diff --git a/src/class/action.nw b/src/class/action.nw new file mode 100644 index 0000000..52ab2bf --- /dev/null +++ b/src/class/action.nw @@ -0,0 +1,105 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +Class Inheritance +================= + + ABSTRACT CONCRETE + + generic + | + `------------------ newindex + | + `------------------ index + | + `-- generic_call + | + `------------ call + | + `------------ selfcall + + +<>= + Action = {} + + -- abstract + <> + <> + + -- concrete + <> + <> + <> + <> +@ + +Abstract Action Classes +======================= + +<>= + Action.generic = class() + + <> + <> + <> + <> + <> + <> + <> + <> + <> + <> + <> + <> + <> + + +<>= + Action.generic_call = class( Action.generic ) + + Action.generic_call.can_return = true + <> + <> + + <> + <> +@ + +Concrete Action Classes +======================= + +<>= + Action.newindex = class( Action.generic ) + + <> + <> + <> + + +<>= + Action.index = class( Action.generic ) + + Action.index.can_return = true + <> + <> + + <> + <> + <> + + +<>= + Action.call = class( Action.generic_call ) + + <> + <> + <> + + +<>= + Action.selfcall = class( Action.generic_call ) + + <> + <> + <> diff --git a/src/class/argv.nw b/src/class/argv.nw new file mode 100644 index 0000000..56d2aa8 --- /dev/null +++ b/src/class/argv.nw @@ -0,0 +1,11 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +<>= + Argv = class() + + <> + <> + <> + <> diff --git a/src/class/callable.nw b/src/class/callable.nw new file mode 100644 index 0000000..1fd9506 --- /dev/null +++ b/src/class/callable.nw @@ -0,0 +1,13 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +<>= + Callable = {} + Callable.generic = class() + Callable.record = class( Callable.generic ) + Callable.replay = class( Callable.generic ) + + <> + <> + <> diff --git a/src/class/controller.nw b/src/class/controller.nw new file mode 100644 index 0000000..40ed6c3 --- /dev/null +++ b/src/class/controller.nw @@ -0,0 +1,30 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +<>= + Controller = class() + + -- Exported methods + <> + <> + <> + <> + <> + <> + <> + <> + <> + <> + + -- Protected methods + <> + <> + <> + <> + <> + <> + <> + <> + <> + <> diff --git a/src/class/mock.nw b/src/class/mock.nw new file mode 100644 index 0000000..ed10e43 --- /dev/null +++ b/src/class/mock.nw @@ -0,0 +1,13 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +@ + +<>= + <> + + <> + <> + <> + <> + <> + <> diff --git a/src/doc/userguide/chapter_controller.nw b/src/doc/userguide/chapter_controller.nw new file mode 100644 index 0000000..7c78d98 --- /dev/null +++ b/src/doc/userguide/chapter_controller.nw @@ -0,0 +1,23 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= += The Controller = + +The controller's main purpose is to store the recorded actions, create mock +objects, switch to replay mode, and verify the completion of the replay +phase. But it is also needed to set or change special action attributes +during recording. + +It is possible, although doubtfully useful, to use several controllers in +parallel during a single unit test. Each controller maintains its own +action list and state, and mock objects remember which controller they +belong to. + +<> +<> +<> +<> +@ diff --git a/src/doc/userguide/chapter_introduction.nw b/src/doc/userguide/chapter_introduction.nw new file mode 100644 index 0000000..33f3434 --- /dev/null +++ b/src/doc/userguide/chapter_introduction.nw @@ -0,0 +1,87 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= += Introduction = +<> + +=== Example === + +This example tests that the insert_data function of the foo module handles +a missing data base table gracefully. + +``` +<> +``` + +First a controller is created. Then three mock objects are created, one for +the sqlite3 module, and two for objects returned by the (simulated) module. + +Then a preloader for the sqlite3 module is installed, which returns the +sqlite3 mock object instead of the actual sqlite3 module. + +In the record phase the expected calls and their return values (or thrown +errors) are recorded. The order is not significant, so this simplified test +will not detect if the close method is called before the execute method. + +In the replay phase the tested module is loaded and executed. It will use +the mock objects instead of the real data base, and if it makes any +unrecorded calls, an error is thrown. + +The verify phase asserts that all recorded actions have been replayed. If +the foo module for example forgets to call the close method, verify throws +an error. + +@ + +<>= +-- Setup +require 'lemock' +local mc = lemock.controller() +local sqlite3 = mc:mock() +local env = mc:mock() +local con = mc:mock() +package.loaded.luasql = nil +package.preload['luasql.sqlite3'] = function () + luasql = {} + luasql.sqlite3 = sqlite3 + return sqlite3 +end + +-- Record +sqlite3() ;mc :returns(env) +env:connect('/data/base') ;mc :returns(con) +con:execute(mc.ANYARGS) ;mc :error('LuaSQL: no such table') +con:close() +env:close() + +-- Replay +mc:replay() +require 'foo' +local res = foo.insert_data(17) +assert(res==false) + +--Verify +mc:verify() +@ + +<>= + function example_simple_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + q = require 'luasql.sqlite3' + function foo.insert_data() + local env = q() + local con = env:connect( '/data/base' ) + local ok, err = pcall( con.execute, con, 'insert foo bar' ) + con:close() + env:close() + return ok + end + return foo + end + <> + end diff --git a/src/doc/userguide/chapter_mock.nw b/src/doc/userguide/chapter_mock.nw new file mode 100644 index 0000000..f16779a --- /dev/null +++ b/src/doc/userguide/chapter_mock.nw @@ -0,0 +1,21 @@ +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= += The Mock Object = + +Mock objects are empty objects with special Lua meta methods that detect +actions performed with the object. What happens depends on the state +(recording or replaying) of the controller which created the mock object. +During recording the mock object adds the action to the controller's list +of recorded actions. During replay the mock object looks for a matching +recorded action that can be replayed, and simulates the action. + +Some action attributes can not be inferred by the mock objects, for example +return values. These attributes have to be added afterwards with special +controller methods, and always affect the last recorded action. + +<> +<> +@ diff --git a/src/doc/userguide/chapter_tricks.nw b/src/doc/userguide/chapter_tricks.nw new file mode 100644 index 0000000..8091270 --- /dev/null +++ b/src/doc/userguide/chapter_tricks.nw @@ -0,0 +1,67 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= += Tricks = + +Mock objects are completely empty, and do not contain any methods or +properties of their own. If they did, that would risk shadowing a name of a +simulated object's method or property. There is however nothing preventing +users from defining methods and properties in mock objects. This way mock +objects can be turned into stubs, or a kind of mock–stub hybrid. + +<> +@ + +<>= +== Method Overloading == + +Lua does not support method overloading, but it can be (and sometimes is) +implemented manually by testing of function arguments. This presents a +problem to LeMock, because it matches exact arguments, and anyargs in not +sufficient. In this case the mock object can be extended with a dispatcher +function. + +=== Example === + +This example shows a mock object with an overloaded add function. The stub +function can not be defined in the usual way, because that would record an +assignment action; it needs to be defined with //rawset//. + +``` +<> +``` +@ + +<>= +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +do +local function add (a, b) + if type(a) == 'number' then + return m.add_number(a, b) + else + return m.add_string(a, b) + end +end +rawset( m, 'add', add ) -- not recorded +end -- do + +m.add_number(1, 2) ;mc :returns(3) +m.add_string('foo', 'bar') ;mc :returns('foobar') + +mc:replay() +assert_equal( 3, m.add(1, 2) ) +assert_equal( 'foobar', m.add('foo', 'bar') ) + +mc:verify() +@ + +<>= + function overloading_test () + <> + end diff --git a/src/doc/userguide/main.nw b/src/doc/userguide/main.nw new file mode 100644 index 0000000..c65b5d0 --- /dev/null +++ b/src/doc/userguide/main.nw @@ -0,0 +1,23 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= +LeMock User Guide +v 0.6 +2009-05-30 + +%!postproc(html): – – +%!postproc(tex): – -- + +<> +<> +<> +<> +@ + +TODO + method reference with syntax + errors and their messages + limitations & future work diff --git a/src/doc/userguide/section_actions.nw b/src/doc/userguide/section_actions.nw new file mode 100644 index 0000000..ec061a9 --- /dev/null +++ b/src/doc/userguide/section_actions.nw @@ -0,0 +1,34 @@ +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= +== Actions == + +Mock objects detect four types of actions: assignment, indexing, method +call, and self call. During replay an action will only match if it is the +very same action, that is, the same type of action performed on the same +mock object with all the same arguments. There are however +[special arguments #anyargs] that can be used during recording. + +``` +<> +``` +@ + +<>= +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +m.x = 17 -- assignment +r = m.x -- indexing +m.x(1,2,3) -- method call +m:x(1,2,3) -- method call +m(1,2,3) -- self call +@ + +<>= + function actions_test () + <> + end diff --git a/src/doc/userguide/section_anyargs.nw b/src/doc/userguide/section_anyargs.nw new file mode 100644 index 0000000..c19e2d7 --- /dev/null +++ b/src/doc/userguide/section_anyargs.nw @@ -0,0 +1,57 @@ +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= +== Anyargs ==[anyargs] + +<> + +Anyarg and anyargs are constants defined in the controller object. + +=== Example === + +This example tests that the fetch_data function of module foo waits a while +and retries when no data is immediately available, and that it updates the +value of lasttime. + +``` +<> +``` +@ + +<>= +require 'lemock' +local mc = lemock.controller() +local con = mc:mock() + +con:poll() ;mc :returns(nil) +con:sleep(mc.ANYARG) +con:poll() ;mc :returns('123.45') +con.lasttime = mc.ANYARG + +mc:replay() +require 'foo' +local res = foo.fetch_data(con) +assert( math.abs(res-123.45) < 0.0005 ) + +mc:verify() +@ + +<>= + function example_anyargs_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + function foo.fetch_data (con) + local res = con:poll() + while not res do + con:sleep( 10 ) + res = con:poll() + end + con.lasttime = os.time() + return tonumber( res ) + end + end + <> + end diff --git a/src/doc/userguide/section_close.nw b/src/doc/userguide/section_close.nw new file mode 100644 index 0000000..2f2e6a6 --- /dev/null +++ b/src/doc/userguide/section_close.nw @@ -0,0 +1,64 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= +== Close == + +Close can be used to simulate state changes in a limited way. When an +action with a close statement is replayed for the first time, it will +permanently block all labels in its close statement, so that actions with +these labels no longer replays. This passes on matching to later actions in +the action list, which may for example have different return values. + +The closing simply blocks the labels, and it has nothing to do with max +replay counts or if closed actions have been satisfied or not. Closing an +unsatisfied action however results in an immediate failure. + +=== Example === + +This example tests that the dump function of module foo calls the myio +functions in a correct order. The read function can be called any number of +times, until it is closed by the close function. + +``` +<> +``` +@ + +<>= +require 'lemock' +local mc = lemock.controller() +local myio = mc:mock() +local fs = mc:mock() + +myio.open('abc', 'r') ;mc :returns(fs) +mc :label('open') + +fs:read(mc.ANYARG) ;mc :returns('data') +mc :atleastonce() :label('read') :depend('open') + +fs:close() ;mc :returns(true) +mc :depend('open') :close('read') + +mc:replay() +require 'foo' +foo.dump(myio, 'abc', 128) + +mc:verify() +@ + +<>= + function close_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + function foo.dump (xio, name, len) + local f = xio.open( name, 'r' ) + f:read( len ) + f:close() + end + end + <> + end diff --git a/src/doc/userguide/section_label_depend.nw b/src/doc/userguide/section_label_depend.nw new file mode 100644 index 0000000..fca2581 --- /dev/null +++ b/src/doc/userguide/section_label_depend.nw @@ -0,0 +1,69 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= +== Label & Depend == + +Dependencies block actions from replaying until other actions have replayed +first. They can be used to verify that actions are being replayed in a +valid order. + +To add dependencies, actions must first be labeled with one or more +//labels//. The same label can be given to several actions. As long as some +action with the label remains unsatisfied, that label is blocked, and all +actions depending on that label will not replay. + +=== Example === + +This (contrived) example tests that function draw_square in module foo +calls all the necessary drawing methods of a square object in a correct +order. Note that there can be more than one correct order. + +``` +<> +``` + +This example demonstrates two different ways of using dependencies. All the +corners have unique labels, because each edge depend on a set of specific +corners. But all the edges have the same label, because the fill operation +only depends on //all// edges have been satisfied. +@ + +<>= +require 'lemock' +local mc = lemock.controller() +local square = mc:mock() + +square:topleft() ;mc :label('tl') +square:topright() ;mc :label('tr') +square:botleft() ;mc :label('bl') +square:botright() ;mc :label('br') +square:leftedge() ;mc :label('edge') :depend('tl', 'bl') +square:rightedge() ;mc :label('edge') :depend('tr', 'br') +square:topedge() ;mc :label('edge') :depend('tl', 'tr') +square:botedge() ;mc :label('edge') :depend('bl', 'br') +square:fill() ;mc :depend('edge') + +mc:replay() +require 'foo' +foo.draw_square( square ) + +mc:verify() +@ + +<>= + function example_depend_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + function foo.draw_square (sq) + sq:botright() sq:topright() sq:rightedge() + sq:botleft() sq:topleft() sq:leftedge() + sq:topedge() sq:botedge() + sq:fill() + end + end + <> + end diff --git a/src/doc/userguide/section_returns_error.nw b/src/doc/userguide/section_returns_error.nw new file mode 100644 index 0000000..33110b5 --- /dev/null +++ b/src/doc/userguide/section_returns_error.nw @@ -0,0 +1,38 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= +== Returns & Error == + +The by far most useful special action attribute is the return value. +Indexing actions can return a single value, while call actions and self +call actions can return a list of values. The return value is set with the +//returns// method, and it is an error to set the return value twice for +the same action. + +For purposes of unit testing it is often useful to simulate errors. All +actions can raise an error, and return an error value (usually a string). +The return value is set with the //error// method. An action can not have +both a return value and raise an error. + +=== Example === +``` +<> +``` +@ + +<>= +require 'lemock' +local mc = lemock.controller() +local m = mc:mock() + +m:foo(17) ;mc :returns(nil, "index out of range") +m:bar(-1) ;mc :error("invalid index") +@ + +<>= + function returns_error_test () + <> + end diff --git a/src/doc/userguide/section_times.nw b/src/doc/userguide/section_times.nw new file mode 100644 index 0000000..a05b31e --- /dev/null +++ b/src/doc/userguide/section_times.nw @@ -0,0 +1,65 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= +== Times == + +The default for a recorded action is to be replayed exactly once. +``times(2)`` changes that to exactly two times, and ``times(1,2)`` changes +it to at least one time and at most two times. + +When the action has been replayed the least count times it is +//satisfied//, which means verify will not complain about it, and it no +longer blocks actions that depend on this action from being replayed. If +the least count is zero the action is automatically satisfied and need not +be replayed at all, i.e., it is optional. + +When the action has been replayed the most count times it will not replay +any more. The most replay count can be set to infinity (``math.huge`` or +``1/0``), in which case the action will never stop replaying. + +``anytimes()`` can be used as an alias for ``times(0,1/0)``, and +``atleastonce()`` can be used as an alias for ``times(1,1/0)``. + +=== Example === + +This example tests that method update is called at least once. + +``` +<> +``` +@ + +<>= +require 'lemock' +local mc = lemock.controller() +local con = mc:mock() + +con:log(mc.ANYARGS) ;mc :anytimes() +con:update('x',3) ;mc :returns(true) :atleastonce() + +mc:replay() +require 'foo' +local watcher = foo.mk_watcher( con ) +watcher:set( 'x', 3 ) + +mc:verify() +@ + +<>= + function example_times_test () + package.loaded.foo = nil + package.preload.foo = function () + foo = {} + function foo.mk_watcher ( con ) + local o = {} + function o:set ( key, val ) + con:update( key, val ) + end + return o + end + end + <> + end diff --git a/src/doc/userguide/unittests.nw b/src/doc/userguide/unittests.nw new file mode 100644 index 0000000..ab89925 --- /dev/null +++ b/src/doc/userguide/unittests.nw @@ -0,0 +1,19 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= + <> + + require 'lunit' + module( 'unit.userguide', lunit.testcase, package.seeall ) + + <> + <> + <> + <> + <> + <> + <> + <> diff --git a/src/doc/webpages.nw b/src/doc/webpages.nw new file mode 100644 index 0000000..59aa292 --- /dev/null +++ b/src/doc/webpages.nw @@ -0,0 +1,297 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +The sources for the web pages are written as independent t2t documents. To +form a cohesive web "site", the pages are included in special wrapper t2t +documents, which use a different set of configurations, and add a header, a +navigation menu bar, and a footer. The wrappers are tangled into the www +directory, the userguide is tangled into the build directory, and the other +sources are unique files in the distribution root directory. + +<>= +MKSHELL = rc + +all:V: htdocs + +wrappers = `{find www -name '*.t2t'} +htmls = ${wrappers:www/%.t2t=htdocs/%.html} + +htdocs:V: $htmls + +$htmls: www/menubar.html +htdocs/COPYRIGHT.html: ../COPYRIGHT +htdocs/DEVEL.html: ../DEVEL +htdocs/HISTORY.html: ../HISTORY +htdocs/README.html: ../README + +htdocs/%.html: www/%.t2t + txt2tags -t html -i www/$stem.t2t -o $target + +htdocs/userguide.html: www/userguide.t2t userguide.t2t + txt2tags -t html --toc --toc-level 2 -i www/userguide.t2t -o $target +@ + +== Wrappers == + +The wrappers assume the source is tangled in a subdirectory, so ../../ is +the path to the root. + +The ID of each page is changed to something unique, so the navigation menu +can identify them. + +<>= +<> +%!postproc(html): 'ID="body"' 'ID="page-README"' +<> += Readme = +%!include: ../../README +<> +@ + +(No footer on copyright page!) + +<>= +<> +%!postproc(html): 'ID="body"' 'ID="page-COPYRIGHT"' +<> += License = +%!include: ../../COPYRIGHT +@ + +<>= +<> +%!postproc(html): 'ID="body"' 'ID="page-HISTORY"' +<> += History = +%!include: ../../HISTORY +<> +@ + +<>= +<> +%!postproc(html): 'ID="body"' 'ID="page-DEVEL"' +<> += Developer Notes = +%!include: ../../DEVEL +<> +@ + +(The user guide is tangled into the build directory.) + +<>= +<> +%!postproc(html): 'ID="body"' 'ID="page-userguide"' +<> +%%toc +%!include: ../userguide.t2t +<> +@ + +The index file is just an empty file with the navigation menu. This is +stupid. FIXME! + +<>= +<> +%!postproc(html): 'ID="body"' 'ID="page-index"' +<> +@ + +== Header and Footer == + +The header contains the page's title (all pages get the same title), and +sets up the configuration. The footer just contains the date for when the +page was generated. + +<>= +LeMock + + +%!includeconf: config.rc +<> +@ + +<>= +-------------------- +%%Date(%Y-%m-%d) +@ + +Use css for layout and navigation menu. + +<>= +%!options: --no-rc +%!style(html): style.css +%!options(html): --css-sugar +@ + +<>= +<> +<> +@ + +Some t2t pages are written with the intent that they should work as plain +text documents, and so they refer to other files by name. In the web pages +real links are preferred, but the words README and DEVEL are too common, so +a special trick with a trailing underscore is used. + +<>= +%!preproc: COPYRIGHT_ "[COPYRIGHT COPYRIGHT.html]" +%!preproc: DEVEL_ "[DEVEL DEVEL.html]" +%!preproc: "build/htdocs/userguide.html" "[the user guide userguide.html]" +@ + +== Style Sheet === + +<>= + <> + <> + <> + <> + <> + +<>= + body { + color: #181818; + background-color: #E0E4F0; + font: normal 10pt sans-serif; + max-width: 30em; + margin: 25pt; + } + .body h1 { + margin: 2em 0em 0em 0em; + font-size: 14pt; + } + .body h2 { + margin: 1.5em 0em 0em 0em; + font-size: 12pt; + } + .body h3 { + margin: 1em 0em 0em 0em; + font-size: 10pt; + } + .body p, .body ul, .body ol { + margin-top: 0.5em; + } + .body li { + margin-top: 0.5em; + } + a { + text-decoration: none; + } + hr { + margin-top: 3em; + } + +<>= + .header h1 { + text-align: center; + padding: 0.3em; + border: 1pt solid black; + } + +<>= + code, pre { + font-family: fixed; + font-style: normal; + font-size: 9pt; + line-height: 9pt; + background-color: #E8ECF8; + } + pre { + padding: 2pt; + } + +<>= + div.toc { + margin-top: 3em; + line-height: 6pt; + } + .toc ul { + padding-left: 1.6em; + margin: 0em; + line-height: 10pt; + } + .toc li { + margin: 0em; + padding: 0em; + list-style-type: none; + } + .toc a { + color: #091; + } + +<>= + #page-HISTORY DL DT { + font-weight: bold; + margin-top: 2em; + } + #page-HISTORY DL DD UL { + margin-top: 0pt; + padding-left: 0pt; + } +@ + +== Navigation Menu Bar == + +The navigation menu bar is implemented with css. + +<>= +%!include(html): ''menubar.html'' +@ + +<>= + +@ + +<>= + #main_menu { + margin: 0; + padding: 0; + } + #main_menu li { + margin: 0; + padding: 0; + display: inline; + } + #main_menu a { + padding: 3px 3px 2px 4px; + text-decoration:none; + font:bold 8pt/8pt Arial, Helvetica, sans-serif; + border: 1px solid #000; + } + #main_menu a:link, + #main_menu a:visited { + color: #fff; + background: #777; + } + #main_menu a:hover { + color: #000; + background: #777; + } + #page-README #main_menu-README a, + #page-COPYRIGHT #main_menu-COPYRIGHT a, + #page-userguide #main_menu-userguide a, + #page-HISTORY #main_menu-HISTORY a, + #page-DEVEL #main_menu-DEVEL a { + color: #000; + background: #aaa; + } + #page-README #main_menu-README a:hover, + #page-COPYRIGHT #main_menu-COPYRIGHT a:hover, + #page-userguide #main_menu-userguide a:hover, + #page-HISTORY #main_menu-HISTORY a:hover, + #page-DEVEL #main_menu-DEVEL a:hover { + color: #000; + background: #aaa; + } + #nav a:active { + color: #000; + background: #aaa; + } diff --git a/src/files.nw b/src/files.nw new file mode 100644 index 0000000..c26aa6d --- /dev/null +++ b/src/files.nw @@ -0,0 +1,32 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= + <> + + module( 'lemock', package.seeall ) + _VERSION = "LeMock 0.6" + _COPYRIGHT = "Copyright (C) 2009 Tommy Pettersson " + + local class, object, qtostring, sfmt, add_to_set + local elements_of_set, value_equal + <> + <> + <> + <> + + <> + + -- All the classes are private + local Action, Argv, Callable, Controller, Mock + <> + <> + <> + <> + <> + + <> + + return _M diff --git a/src/helperfunctions.nw b/src/helperfunctions.nw new file mode 100644 index 0000000..cbb9616 --- /dev/null +++ b/src/helperfunctions.nw @@ -0,0 +1,65 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +class and object +---------------- + +These functions are mostly to enhance the reading of the code. + +<>= + function object (class) + return setmetatable( {}, class ) + end + function class (parent) + local c = object(parent) + c.__index = c + return c + end +@ + +value_equal +----------- + +NaN is always numerically not-equal everything, but for arguments and +return values we want a symbolic comparison. + +<>= + function value_equal (a, b) + if a == b then return true end + if a ~= a and b ~= b then return true end -- NaN == NaN + return false + end +@ + +Sets +---- + +Sets are used in Actions to store labels and dependencies, which are +usually very short, and often not used at all, so it is worth some extra +trouble to optimize for size. + +The implementation uses arrays, and no array (nil) can represent the empty +set. + +<>= + function add_to_set (o, setname, element) + if not o[setname] then + o[setname] = {} + end + local l = o[setname] + + for i = 1, #l do + if l[i] == element then return end + end + l[#l+1] = element + end + function elements_of_set (o, setname) + local l = o[setname] + local i = l and #l+1 or 0 + return function () + i = i - 1 + if i > 0 then return l[i] end + end + end diff --git a/src/main.nw b/src/main.nw new file mode 100644 index 0000000..5fb454b --- /dev/null +++ b/src/main.nw @@ -0,0 +1,761 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +Introduction +############ + +<>= + +Mock objects replace difficult external objects during unit testing by +simulating the behaviors of the replaced objects. This is done by first +recording actions and their responses with the mock objects, and then +switching to replay mode. During replay mode the mock objects simulate the +replaced objects by looking up actions and replaying the recorded +responses, and finally verifying that all expected actions where completely +replayed. + +Actions are stored in a list in a special controller object. During replay +the list is searched in recording order for the first matching action that +can be replayed. + +Restrictions on the actions can be inserted during the recording phase. An +action can have a maximum count of how many times it will be replayed, and +a minimum count of how many times it must be replayed to be satisfied. An +action can depend on any set of other actions, and can not be replayed +before all of its depended actions are satisfied. An action can close any +set of actions when it is replayed, which stops all further replaying of +the closed actions. This is good for simulating state changes. +@ + +Manage Actions +############## + +The Action List +=============== + +Actions are stored in a list in a special Controller object. To keep it +simple, the list is a simple array table. The controller has an +[[add_action]] method, and a [[lookup]] method. Actions are added at the +end, and looked up from the beginning. + + +add_action +---------- + +<>= + function add_action_at_the_end_test () + mc:add_action( 7 ) + mc:add_action( mc ) + assert_equal( 7, mc.actionlist[1] ) + assert_equal( mc, mc.actionlist[2] ) + end + +<>= + function Controller:add_action (a) + assert( a ~= nil, "lemock internal error" ) -- breaks array property + table.insert( self.actionlist, a ) + end +@ + +lookup and actions +------------------ + +It should be easy to add new action types, so the [[lookup]] method +delegates matching to the [[match]] method of the Action objects in the +list. If there is no match an error is thrown with a list of all actions +that could have been matched. + +<>= + function lookup_returns_first_matching_action_test () + local Fake_action + <> + local a1 = Fake_action:new(1) + local a2 = Fake_action:new(2) + local a3 = Fake_action:new(1) + local ok, err = pcall( function() mc:lookup( a1 ) end ) + assert_false( ok, "match in empty list" ) + assert_match( "Unexpected action ", err ) + mc:add_action( a1 ) mc:add_action( a2 ) mc:add_action( a3 ) + local ok, err = pcall( function() mc:lookup( a1 ) end ) + assert_false( ok, "should not match any action" ) + assert_match( "Unexpected action ", err ) + assert_equal( a1, mc:lookup( a2 ), "did not find first match" ) + end + +<>= + function Controller:lookup (actual) + for action in self:actions() do + if action:match( actual ) then + return action + end + end + <> + error( sfmt( "Unexpected action %s, expected:\n%s\n" + , actual:tostring() + , table.concat(expected,'\n') + ) + , 0 + ) + end +@ +There is no point in listing index actions for callables, since they will +just mirror the call actions. + +Sorting the list will make it easier for the user to conclude that an +expected action is "missing", because there will be a definite place in the +list where the action would have been. + +<>= + local expected = {} + for _, a in ipairs( self.actionlist ) do + if a:is_expected() and not a.is_callable then + expected[#expected+1] = a:tostring() + end + end + table.sort( expected ) + if #expected == 0 then + expected[1] = "(Nothing)" + end +@ +Many functions iterate over all the actions in the action list. + +<>= + function actions_dont_iterate_empty_list_test () + for a in mc:actions() do + fail( "iterates on empty list" ) + end + end + function actions_iterate_over_entire_list_exactly_once_test () + local l = { {},{},{} } + for _, a in ipairs( l ) do + mc:add_action( a ) + end + for a in mc:actions() do + assert_nil( a.check ) + a.check = true + end + for _, a in ipairs( l ) do + assert_true( a.check ) + end + end + +<>= + function Controller:actions (q) + local l = self.actionlist + local i = 0 + return function () + i = i + 1 + return l[i] + end + end +@ + +The Controller object will allow the user to set return values or +restrictions on the last recorded action. + +last +---- + +<>= + function get_last_action_returns_last_element_test () + local l = { 'a', 'foo', 17 } + for i = 1, #l do + mc:add_action( l[i] ) + local res = mc:get_last_action() + assert_equal( l[i], res ) + end + end + function get_last_action_fails_on_empty_list_test () + local ok, err = pcall( function() mc:get_last_action() end ) + assert_false( ok, "Found last action in empty list" ) + assert_match( "No action is recorded yet", err ) + end + +<>= + function Controller:get_last_action () + local l = self.actionlist + if #l == 0 then + error( "No action is recorded yet.", 0 ) + end + return l[#l] + end +@ + +Actions +======= + +Each Action type is implemented as a specialized class so it is easy to add +new action types. The action class is responsible for storing information +and state. The Mock class and Callable class are responsible for catching +and recording/replaying the action with help from the information in the +Action object. + +Action types differ in what information they need to store, and therefore +how they are constructed, matched by lookup, and printed as strings. The +concrete or extended implementations of the methods [[new]], [[match]], and +[[tostring]], are therefore implemented together with the corresponding +Mock and Callable meta methods responsible for catching the action, +arranged in separate source files in the action/ directory. + + +New +--- + +New Action objects are created with default [[min_replays]] and +[[max_replays]] equal to one, which means each recorded action is expected +to be replayed exactly once. This method will be extended by the the +concrete Action class types. + +<>= + function new_action_has_right_default_values_test () + assert_equal( 0, a.replay_count ) + assert_equal( 1, a.min_replays ) + assert_equal( 1, a.max_replays ) + end + +<>= + function Action.generic:new (mock) + local a = object( self ) + a.mock = mock + a.replay_count = 0 + a.min_replays = 1 + a.max_replays = 1 + return a + end +@ + +match and is_expected +--------------------- + +The [[match]] method takes an Action object as key for the search, because +it is a convenient way to pass all the needed properties. The Action +objects in the action list should compare themselves with the key to see if +they can replay such an action. The state properties of the key object are +of course not used, but the state of the actions in the list is important +for if the actions can be replayed or not. The method [[is_expected]] +examines if the state allows the action to be replayed. The [[match]] +method will be extended by the concrete Action class types. + +<>= + function expect_unreplayed_action_test () + assert_true( a:is_expected() ) + end + +<>= + function Action.generic:is_expected () + return self.replay_count < self.max_replays + and not self.is_blocked + and not self.is_closed + end + +<>= + function match_unreplayed_test () + assert_true( a:match( a )) + end + function match_rejects_replayed_action_test () + a.replay_count = 1 + assert_false( a:match( a )) + end + function match_rejects_wrong_action_type_test () + -- Fake different type + local B = class( A ) + local b = B:new() + assert_false( a:match( b )) + end + +<>= + function Action.generic:match (key) + if getmetatable(self) ~= getmetatable(key) then return false end + if self.mock ~= key.mock then return false end + return self:is_expected() + end +@ + +replay_action +------------- + +The [[replay]] method updates the state of an Action. It has nothing to do +with performing the action, which is done by the Mock class and Callable +class. It is an internal error to call this method with an action that is +not expected. + +<>= + function replay_action_test () + local a = A:new() + mc:add_action( a ) + assert_true( a:is_expected() ) + assert_false( a:is_satisfied() ) + mc:replay_action( a ) + assert_false( a:is_expected() ) + assert_true( a:is_satisfied() ) + assert_equal( 1, a.replay_count ) + end + +<>= + function Controller:replay_action ( action ) + assert( action:is_expected(), "lemock internal error" ) + assert( action.replay_count < action.max_replays, "lemock internal error" ) + local was_satisfied = action:is_satisfied() + action.replay_count = action.replay_count + 1 + if not was_satisfied and action.labellist and action:is_satisfied() then + self:update_dependencies() + end + if action.closelist then + self:close_actions( action:closes() ) + end + end +@ + +is_satisfied and assert_satisfied +--------------------------------- + +When the replay phase is finished the Controller needs to verify that all +the actions in the action list have been satisfied. If some action is not +satisfied it is an error, so [[assert_satisfied]] must also throw an +appropriate error. + +<>= + function unreplayed_action_is_not_satisfied_test () + assert_false( a:is_satisfied() ) + end + function assert_satisfied_unreplayed_action_fails_test () + local ok, err = pcall( function() a:assert_satisfied() end ) + assert_false( ok, "unreplayed action was satisfied" ) + assert_match( "Wrong replay count 0", err ) + end + +<>= + self.min_replays <= self.replay_count + +<>= + function Action.generic:is_satisfied () + return <> + end + +<>= + function Action.generic:assert_satisfied () + assert( self.replay_count <= self.max_replays, "lemock internal error" ) + if not (<>) then + error( sfmt( "Wrong replay count %d (expected %d..%d) for %s" + , self.replay_count + , self.min_replays, self.max_replays + , self:tostring() + ) + , 0 + ) + end + end +@ + +Catching and Simulating Actions +############################### + +Three classes work in tight connection to catch, record, and replay +actions. They are [[Controller]], [[Mock]], and [[Callable]]. + +The Controller is the main class, and it stores the action list with all +the Actions. The Controller is used to record explicit information that can +not be caught in an automatic manner by the Mock or Callable objects, such +as retur values. It is used to record meta information such as replay +limits and replay order dependencies. It is used to create the Mock +objects, to switch to replay mode, and to verify the completeness of the +replay phase. + +The Mock must be completely empty of methods and properties, because we +want to catch all of them, without accidentally shadowing any names. So it +can only use Lua meta methods, like [[__index]] and [[__newindex]]. Because +of this limitation it is not possible to store a reference to the +Controller in the Mock object, so we need a map for this. + +<>= + local mock_controller_map = setmetatable( {}, {__mode='k'} ) +@ +When a method of a Mock object is called there are actually two actions. +First the method's name is indexed, and then the returned object is called. +So the Mock [[__index]] meta method must record the index part, *and* return +something with a [[__call]] meta method that can record the call part. This +something is a [[Callable]] object. + + +Setup Phase +=========== + +The Controller +-------------- + +To keep the user interface clean the Controller class itself is never +exported. The module function [[controller]] creates a new local controller +object, and creates and returns a wrapper for it with the exported methods. + +<>= + function controller () + local exported_methods = { + 'anytimes', + 'atleastonce', + 'close', + 'depend', + 'error', + 'label', + 'mock', + 'new', + 'replay', + 'returns', + 'times', + 'verify', + } + local mc = Controller:new() + local wrapper = {} + for _, method in ipairs( exported_methods ) do + wrapper[ method ] = function (self, ...) + return mc[ method ]( mc, ... ) + end + end + wrapper.ANYARG = Argv.ANYARG + wrapper.ANYARGS = Argv.ANYARGS + return wrapper + end + +<>= + function Controller:new () + local mc = object( self ) + mc.actionlist = {} + mc.is_recording = true + return mc + end +@ + +The Mock +-------- + +The Mock class has two modes, one for recording and one for replaying. +These two modes are implemented as two different metatables. The [[class] +helper functions is not used to create them, because it sets [[__index]] to +be a self reference. The [[__index]] meta method, and other meta methods, +of the two Mock metatables are used for catching actions, and are defined +in the corresponding action source files. + +<>= + Mock = { record={}, replay={} } -- no self-referencing __index! +@ +Because the Mock must be completely empty it can not have a [[new]] method. +So the Controller has a [[mock]] method that creates and returns new mock +objects, and maps them to itself in [[mock_controller_map]]. The Mock is +created in record mode, but will be switched to replay mode later by the +Controller. + +<>= + function create_completely_empty_mock_test () + for k, v in pairs( m ) do + fail( "Mock should be empty but contains "..tostring(k) ) + end + end + function create_mock_during_replay_fails_test () + mc:replay() + local ok, err = pcall( function() mc:mock() end ) + assert_false( ok, "mock() succeeded" ) + assert_match( "New mock during replay.", err ) + end + +<>= + function Controller:mock () + if not self.is_recording then + error( "New mock during replay.", 2 ) + end + local m = object( Mock.record ) + mock_controller_map[m] = self + return m + end +@ + +The Callable +------------ + +A Callable is only created as the result of an index action. It is not +known at the time of the index action if the index will be called, but if +it is, the call action must update the index action with this information, +and therefore needs a reference to it. + +<>= + function Callable.generic:new ( index_action ) + local f = object( self ) + f.action = index_action + return f + end +@ + +Record Phase +============ + +Return Values and Callables +--------------------------- + +Index action can either have a return value or be callable, in which case +they return a Callable object. When the call action is recorded the +corresponding index action must be marked as [[is_callable]], so that it +knows it shall return a Callable object during replay mode. + +Index actions that return callables have their replay count limits set to +any-times, because all control adjustments will be made to the call action, +and it should not matter how many times the callable is retrieved from the +mock, but only how many times and in what order it is called. + +If the retrieval count and/or order is important it is still possible to +simulate such behavior with an index action returning a mock object. Then +the index action can be controlled, and the call action can instead be +recorded with the selfcall action of the returned mock object. + +<>= + function returns_on_empty_list_fails_test () + local ok, err = pcall( function() mc:returns(nil) end ) + assert_false( ok, "returns called on nothing" ) + assert_match( "No action is recorded yet.", err ) + end + function returns_make_call_fail_test () + local tmp = m.foo ;mc:returns(1) + local ok, err = pcall( function() tmp(2) end ) + assert_false( ok, "called index with returnvalue" ) + assert_match( "Can not call foo. It has a returnvalue.", err ) + end + function callable_index_replays_anytimes_test () + local tmp = m.foo() + mc:replay() + tmp = m.foo + tmp = m.foo + tmp = m.foo() + mc:verify() + end + +<>= + function Controller:make_callable (action) + if action.has_returnvalue then + error( "Can not call "..action.key..". It has a returnvalue.", 0 ) + end + action.is_callable = true + action.min_replays = 0 + action.max_replays = math.huge + end +@ +Action classes that can return a value have the [[can_return]] property +set, and they implement the [[set_returnvalue]] and [[get_returnvalue]] +methods. + +If an index action is marked as [[is_callable]] it ought to be followed by +a call action responsible for making it callable, and since it is no longer +the last action it should be impossible to set a returnvalue for the index +action. Doing so is an internal error. + +<>= + function returns_during_replay_fails_test () + local tmp = m.foo + mc:replay() + local ok, err = pcall( function() mc:returns(1) end ) + assert_false( ok, "returns() succeeded during replay" ) + assert_match( "Returns called during replay.", err ) + end + function returns_on_nonreturning_action_fails_test () + m.foo = 1 -- assignments can't return + local ok, err = pcall( function() mc:returns(0) end ) + assert_false( ok, "returns() succeeded on non-returning action" ) + assert_match( "Previous action can not return anything.", err ) + end + function returns_twice_fails_test () + local tmp = m.foo ;mc:returns(1) + local ok, err = pcall( function() mc:returns(2) end ) + assert_false( ok, "duplicate returns() succeeded" ) + assert_match( "Returns and/or Error called twice for same action.", err ) + end + +<>= + function Controller:returns (...) + if not self.is_recording then + error( "Returns called during replay.", 2 ) + end + local action = self:get_last_action() + assert( not action.is_callable, "lemock internal error" ) + if not action.can_return then + error( "Previous action can not return anything.", 2 ) + end + if action.has_returnvalue or action.throws_error then + error( "Returns and/or Error called twice for same action.", 2 ) + end + action:set_returnvalue(...) + return self -- for chaining + end +@ + +Throwing Errors +--------------- + +Any action can be made to throw a recorded error when replayed. For +example, a newindex action could simulate an assignment to a userdata och +table with a metatable that is supposed to throw an error on assignments. + +<>= + function error_during_replay_fails_test () + local tmp = m.foo + mc:replay() + local ok, err = pcall( function() mc:error(1) end ) + assert_false( ok, "error() succeeded during replay" ) + assert_match( "Error called during replay.", err ) + end + function error_twice_fails_test () + local tmp = m.foo ;mc:error(1) + local ok, err = pcall( function() mc:error(2) end ) + assert_false( ok, "duplicate error() succeeded" ) + assert_match( "Returns and/or Error called twice for same action.", err ) + end + function error_plus_returns_fails_test () + local tmp = m.foo ;mc:returns(1) + local ok, err = pcall( function() mc:error(2) end ) + assert_false( ok, "both error and returns succeeded" ) + assert_match( "Returns and/or Error called twice for same action.", err ) + end + +<>= + function Controller:error (value) + if not self.is_recording then + error( "Error called during replay.", 2 ) + end + local action = self:get_last_action() + if action.has_returnvalue or action.throws_error then + error( "Returns and/or Error called twice for same action.", 2 ) + end + action.throws_error = true + action.errorvalue = value + return self -- for chaining + end +@ + +Switching from Record Mode to Replay Mode +========================================= + +Switching to replay mode is done by changing all Mock objects' matatables. +It is an error to call [[replay]] twice on the same Controller. + +Dependency information (see restrictions.nw) is not calculated in real +time. It is updated when needed, for example before starting the replay +phase. This is also a good point to check for dependency cycles. + +<>= + function replay_twice_fails_test () + mc:replay() + local ok, err = pcall( function() mc:replay() end ) + assert_false( ok, "replay succeeded twice" ) + assert_match( "Replay called twice.", err ) + end + function multiple_controllers_test () + local mc2 = lemock.controller() + local m2 = mc2:mock() + + -- m -- -- m2 -- + m.foo = 1 + mc:replay() + m2.bar = 2 + m.foo = 1 + mc2:replay() + mc:verify() + m2.bar = 2 + mc2:verify() + end + +<>= + function replay_test () + assert_true( mc.is_recording ) + mc:replay() + assert_false( mc.is_recording ) + end + +<>= + function Controller:replay () + if not self.is_recording then + error( "Replay called twice.", 2 ) + end + self.is_recording = false + for m, mc in pairs( mock_controller_map ) do + if mc == self then + setmetatable( m, Mock.replay ) + end + end + self:update_dependencies() + self:assert_no_dependency_cycles() + end +@ + +Replay Phase +============ + +The key point with the replay phase is that unexpected actions should fail. + +<>= + function replay_in_any_order_test () + m.a = 1 + m.b = 2 + m.c = 3 + mc:replay() + m.c = 3 + m.a = 1 + m.b = 2 + mc:verify() + end + function replaying_unexpected_action_fails_test () + mc:replay() + local ok, err = pcall( function() m:somethingelse() end ) + assert_false( ok, "unexpected replay succeeded" ) + assert_match( "Unexpected action index somethingelse", err ) + end +@ +There is an error that would be very hard to track down if it is not +detected. During record mode the Mock returns recording Callables, but +during replay mode it returns replaying Callables. If the client caches a +Callable in record mode and uses it during replay mode, it would not fail +straight away as desired (if undetected), but record a false action which +would mess up the action list and cause some later action or the final +verify to pass or fail with a perplexing error message. + +<>= + function cached_recording_callable_fails_during_replay_test () + local tmp = m.foo ; tmp() + mc:replay() + local ok, err = pcall( function() tmp() end ) + assert_false( ok, "Cached callable not detected" ) + assert_match( "client uses cached callable from recording", err ) + end +@ + +Verify Completeness Phase +========================= + +Unknown or misplaced actions can be detected during the replay phase, but +expected actions that are not replayed can not be detected until the replay +phase is finished. That is why the verify phase is needed, and it simply +asserts that all recorded actions have been satisfied. + +<>= + function verify_during_record_phase_fails_test () + local ok, err = pcall( function() mc:verify() end ) + assert_false( ok, "Verify succeeded" ) + assert_match( "Verify called during record.", err ) + end + function verify_replayed_actionlist_test () + mc:replay() + mc:verify() + end + function verify_unreplyed_actionlist_fails_test () + local tmp = m.foo + mc:replay() + local ok, err = pcall( function() mc:verify() end ) + assert_false( ok, "Verify succeeded" ) + assert_match( "Wrong replay count 0 ", err ) + end + +<>= + function Controller:verify () + if self.is_recording then + error( "Verify called during record.", 2 ) + end + for a in self:actions() do + a:assert_satisfied() + end + end diff --git a/src/misc.nw b/src/misc.nw new file mode 100644 index 0000000..6d95ac3 --- /dev/null +++ b/src/misc.nw @@ -0,0 +1,30 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +<>= + ------ THIS FILE IS TANGLED FROM LITERATE SOURCE FILES ------ + -- Copyright (C) 2009 Tommy Pettersson + -- See terms in file COPYRIGHT, or at http://lemock.luaforge.net + +<>= + Fake_action = class() + function Fake_action:new (x) + local a = object(Fake_action) + a.x = x + return a + end + function Fake_action:match (q) + return self.x < q.x + end + function Fake_action:is_expected () + return true + end + function Fake_action:tostring () + return '' + end + function Fake_action:blocks () + return function () end + end + Fake_action.depends = Fake_action.blocks diff --git a/src/restrictions.nw b/src/restrictions.nw new file mode 100644 index 0000000..7380089 --- /dev/null +++ b/src/restrictions.nw @@ -0,0 +1,632 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +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. + +<>= + 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 + +<>= + 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 +---------------- + +<>= + 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 + +<>= + 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. + +<>= + 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 + +<>= + 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 + +<>= + 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. + +<>= + 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 + +<>= + function Action.generic:add_label (label) + add_to_set( self, 'labellist', label ) + end + +<>= + 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 + +<>= + 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. + +<>= + 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. + +<>= + 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. + +<>= + 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 + +<>= + 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 +------------------- + +<>= + 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 + +<>= + 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 + +<>= + function Action.generic:add_depend (d) + add_to_set( self, 'dependlist', d ) + end + +<>= + function Action.generic:depends () + return elements_of_set( self, 'dependlist' ) + end +@ + +Updating dependencies and detecting cycles +------------------------------------------ + +<>= + 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. + +<>= + 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 +===== + +<>= + 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 + +<>= + 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 +------------- + +<>= + 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 + +<>= + function Action.generic:add_close (label) + add_to_set( self, 'closelist', label ) + end +@ + +Perform closes +-------------- + +<>= + 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 + +<>= + function Action.generic:closes () + return elements_of_set( self, 'closelist' ) + end diff --git a/src/tostring.nw b/src/tostring.nw new file mode 100644 index 0000000..8d9c1e6 --- /dev/null +++ b/src/tostring.nw @@ -0,0 +1,182 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +Convertng Objects to Strings +############################ + +When replay or verification fails there should be an explanation that shows +what the problem is. One way is to list the expected actions. Therefore +each Action type has a [[tostring]] method. The output tries to mimic what +the missing expression might have looked like in the failing code. + + +Helper functions qtostring and sfmt +=================================== + +[[sfmt]] is to make the code less verbose. [[qtostring]] is a wrapper for +[[tostring]] which adds quoting of string values to make error messages +easier to understand. + +<>= + sfmt = string.format + function qtostring (v) + if type(v) == 'string' then + return sfmt( '%q', v ) + else + return tostring( v ) + end + end +@ + +Action Newindex +================ + +<>= + function newindex_tostring_test () + local a = Action.newindex:new( {}, 'key', 7 ) + assert_equal( 'newindex key = 7', a:tostring() ) + a = Action.newindex:new( {}, true, '7' ) + assert_equal( 'newindex true = "7"', a:tostring() ) + end + +<>= + function Action.newindex:tostring () + return sfmt( "newindex %s = %s" + , tostring(self.key) + , qtostring(self.val) + ) + end +@ + +Action Index +============ + +<>= + function index_tostring_test () + local a = Action.index:new( {}, true ) + assert_equal( 'index true', a:tostring() ) + a:set_returnvalue('"false"') + assert_equal( 'index true => "\\"false\\""', a:tostring() ) + end + function callable_index_tostring_test () + local a = Action.index:new( {}, 'f' ) + a.is_callable = true + assert_equal( 'index f()', a:tostring() ) + end + +<>= + function Action.index:tostring () + local key = 'index '..tostring( self.key ) + if self.has_returnvalue then + return sfmt( "index %s => %s" + , tostring( self.key ) + , qtostring( self.returnvalue ) + ) + elseif self.is_callable then + return sfmt( "index %s()" + , tostring( self.key ) + ) + else + return sfmt( "index %s" + , tostring( self.key ) + ) + end + end +@ + +Action Call +=========== + +<>= + function call_tostring_test () + local a = Action.call:new( {}, 'foo', 1, '"', 3 ) + assert_equal( 'call foo(1,"\\"",3)', a:tostring() ) + a:set_returnvalue( 'false', false ) + assert_equal( 'call foo(1,"\\"",3) => "false",false', a:tostring() ) + end + +<>= + function Action.call:tostring () + if self.has_returnvalue then + return sfmt( "call %s(%s) => %s" + , tostring(self.key) + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "call %s(%s)" + , tostring(self.key) + , self.argv:tostring() + ) + end + end +@ + +Action Selfcall +=============== + +<>= + function selfcall_tostring_test () + local a = Action.selfcall:new( {}, 1, '"', nil ) + assert_equal( 'selfcall (1,"\\"",nil)', a:tostring() ) + a:set_returnvalue( 'false', false ) + assert_equal( 'selfcall (1,"\\"",nil) => "false",false', a:tostring() ) + end + +<>= + function Action.selfcall:tostring () + if self.has_returnvalue then + return sfmt( "selfcall (%s) => %s" + , self.argv:tostring() + , self.returnvalue:tostring() + ) + else + return sfmt( "selfcall (%s)" + , self.argv:tostring() + ) + end + end +@ + +Class Argv +========== + +Argument lists are converted without surrounding parentheses, because they +can be used as multiple return values as well as call arguments. When they +are used as call arguments, the invoker will have to add the parentheses. + +<>= + function tostring_test () + assert_equal( '', Argv:new() :tostring() ) + assert_equal( '""', Argv:new('') :tostring() ) + assert_equal( 'nil,nil', Argv:new(nil,nil) :tostring() ) + assert_equal( '"false",false', Argv:new('false',false) :tostring() ) + assert_equal( '1,2,3', Argv:new(1,2,3) :tostring() ) + assert_equal( '1,ANYARG,3', Argv:new(1,Argv.ANYARG,3):tostring() ) + assert_equal( 'ANYARGS', Argv:new(Argv.ANYARGS) :tostring() ) + assert_equal( '7,0,ANYARGS', Argv:new(7,0,Argv.ANYARGS):tostring() ) + end + +<>= + function Argv:tostring () + local res = {} + local function w (v) + res[#res+1] = qtostring( v ) + end + local av, ac = self.v, self.len + for i = 1, ac do + if av[i] == Argv.ANYARG then + res[#res+1] = 'ANYARG' + elseif av[i] == Argv.ANYARGS then + res[#res+1] = 'ANYARGS' + else + w( av[i] ) + end + if i < ac then + res[#res+1] = ',' -- can not use qtostring in w() + end + end + return table.concat( res ) + end diff --git a/src/unittestfiles.nw b/src/unittestfiles.nw new file mode 100644 index 0000000..e986c9e --- /dev/null +++ b/src/unittestfiles.nw @@ -0,0 +1,169 @@ +Lua Easy Mock -- LeMock +Copyright (C) 2009 Tommy Pettersson +See terms in file COPYRIGHT, or at http://lemock.luaforge.net +@ + +module +------ + +<>= + <> + + require 'lunit' + module( 'unit.module', lunit.testcase, package.seeall ) + + require 'lemock' + + local mc, m + + function setup () + mc = lemock.controller() + m = mc:mock() + end + + <> + <> + <> + <> + <> + <> + <> + <> + <> + + <> + <> + <> + <> + <> + <> +@ + +controller +---------- + +<>= + <> + + require 'lunit' + module( 'unit.controller', lunit.testcase, package.seeall ) + + local class, object, qtostring, sfmt, add_to_set, elements_of_set + <> + <> + <> + + <> + + local Controller, Action + <> + <> + + local A = Action.generic + Action = nil -- only allow generic action + function A:tostring () return '' end + + local mc + + function setup () + mc = Controller:new() + end + + <> + <> + <> + <> + <> + <> + <> +@ + +argv +---- + +<>= + <> + + require 'lunit' + module( 'unit.argv', lunit.testcase, package.seeall ) + + local class, object, value_equal, sfmt, qtostring + <> + <> + <> + + local Argv + <> + + <> + <> + <> + <> +@ + +action_generic +-------------- + +<>= + <> + + require 'lunit' + module( 'unit.action_generic', lunit.testcase, package.seeall ) + + local class, object, qtostring, sfmt, add_to_set, elements_of_set + <> + <> + <> + + local Action, Argv + <> + <> + + local A = Action.generic + Action = nil -- only allow generic action + function A:tostring () return "" end + + local a + + function setup () + a = A:new() + end + + <> + <> + <> + <> + <> + <> + <> +@ + +action +------ + +<>= + <> + + require 'lunit' + module( 'unit.action', lunit.testcase, package.seeall ) + + local class, object, qtostring, sfmt + <> + <> + <> + + local Action, Argv + <> + <> + + <> + <> + <> + <> + <> + <> + <> + <> + <> + <> + <> diff --git a/tools/autotangle b/tools/autotangle new file mode 100644 index 0000000..88e6a8d --- /dev/null +++ b/tools/autotangle @@ -0,0 +1,28 @@ +#!/usr/bin/env rc + +nl=' +' +tf=`tempfile + +for (f in ``($nl){noroots $* |sed 's/^<<\(.*\)>>$/\1/'}) { + if (~ $f *' '*) { + echo >[1=2] 'Skipping bad root <<'^$^f^'>>' + } else { + d=`{dirname $f} if (! test -d $d) mkdir -p $d + switch (`{basename $f}) { + case *.c *.h; o=(-L'#line %L "%F"%N') + case *.lua; o=(-L'-- %F:%L%N') + case *.sh; o=(-L'# %F:%L%N') + case mkfile; o=(-t8) + case *; o=() + } + notangle $o -R$f $* >$tf + if (! cmp -s $tf $f) { + echo Updating $f + rm -f $f + cat $tf > $f + chmod a-w $f + } + } +} +rm $tf