├── README.md ├── calabash.lua ├── cbsh ├── features ├── generator.feature ├── parser.feature └── steps.lua └── rockspecs ├── calabash-0.1.1-1.rockspec └── calabash-scm-1.rockspec /README.md: -------------------------------------------------------------------------------- 1 | Calabash - BDD DSL for Lua 2 | ========================== 3 | 4 | Calabash is a domain-specific language for behavior-driven-development, 5 | modelled after [Cucumber](http://cukes.info/). 6 | 7 | Using calabash 8 | -------------- 9 | 10 | First, write a .feature file (in a features directory in your 11 | project), then define the steps in features/steps.lua, then write code 12 | to pass those tests. 13 | 14 | For a good example of how to use calabash, take a look in the features 15 | directory in this repository. 16 | 17 | Run the tests by running: 18 | 19 | $ cbsh 20 | 21 | Right now there are no command-line options; it just runs all the 22 | tests in the features directory and prints out telescope's more 23 | verbose output. 24 | 25 | ## License ## 26 | 27 | The MIT License 28 | 29 | Copyright (c) 2011 [Paul Bonser](mailto:misterpib@gmail.com) 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining a copy of 32 | this software and associated documentation files (the "Software"), to deal in 33 | the Software without restriction, including without limitation the rights to 34 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 35 | of the Software, and to permit persons to whom the Software is furnished to do 36 | so, subject to the following conditions: 37 | 38 | The above copyright notice and this permission notice shall be included in all 39 | copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 43 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 44 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 45 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 46 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 47 | SOFTWARE. -------------------------------------------------------------------------------- /calabash.lua: -------------------------------------------------------------------------------- 1 | local lpeg = require 'lpeg' 2 | local unpack = unpack 3 | local telescope = require 'telescope' 4 | local _G = _G 5 | 6 | module(..., package.seeall) 7 | 8 | local locale = lpeg.locale() 9 | local C, Cb, Cg, Ct, P, S, V = lpeg.C, lpeg.Cb, lpeg.Cg, lpeg.Ct, lpeg.P, lpeg.P, lpeg.V 10 | 11 | local space = locale.space 12 | local newline = S"\n" + S"\r\n" 13 | local i_space = (space - newline)^0 -- ignored space 14 | local non_space = (1 - space)^1 -- group of at least one non-space character 15 | 16 | local function concat_lines(...) return table.concat(..., '\n') end 17 | 18 | local function format_hash(rows) 19 | local hashes = {} 20 | local keys 21 | for i, values in pairs(rows) do 22 | if i == 1 then 23 | keys = values 24 | else 25 | local hash = {} 26 | for i=1,#keys do 27 | hash[keys[i]] = values[i] 28 | end 29 | table.insert(hashes, hash) 30 | end 31 | end 32 | return hashes 33 | end 34 | 35 | function format_multiline(multi) 36 | local lines = {} 37 | local indent = (P(multi.indent) * C(P(1)^0)) + C(P(1)^0) 38 | for _, line in pairs(multi.lines) do 39 | table.insert(lines, lpeg.match(indent, line)) 40 | end 41 | return table.concat(lines, '\n') 42 | end 43 | 44 | G = Ct{ 45 | "Feature", 46 | Tag = space^0 * '@' * C((1 - space)^1) * space^1, 47 | Tags = Cg(Ct(V'Tag'^0), 'tags'), 48 | Name = Cg((1 - newline)^1, 'name'), 49 | Feature = V'Tags' * (P"Feature:" * space^0 * V'Name' * (newline * V'Description')^-1 * 50 | Cg(Ct(V'Scenario'^0), 'scenarios')), 51 | Description = Cg(Ct(V'PlainLine'^1) / concat_lines, 52 | 'description'), 53 | PlainLine = i_space * Cg((1 - newline - '|' - '"""' - "'''") * (1 - newline)^1) * newline, 54 | 55 | HashValue = Cg(((non_space - '|') * (i_space * (non_space - '|'))^0)^0), 56 | HashtableLine = Ct(i_space * P'|' * (i_space * V'HashValue' * i_space * '|')^1 * i_space * newline), 57 | Hashtable = Ct(V'HashtableLine'^1) / format_hash, 58 | 59 | MultiSLines = Cg(Ct((Cg((1 - P"'''" - newline)^0) * newline)^0), 'lines'), 60 | MultiDLines = Cg(Ct((Cg((1 - P'"""' - newline)^0) * newline)^0), 'lines'), 61 | MultiSingle = P"'''" * newline * V'MultiSLines' * i_space * P"'''", 62 | MultiDouble = P'"""' * newline * V'MultiDLines' * i_space * P'"""', 63 | Multiline = Ct(Cg(i_space, 'indent') * (V'MultiSingle' + V'MultiDouble') * newline) / format_multiline, 64 | 65 | Step = Ct(Cg(V'PlainLine', 'name') * Cg(V'Hashtable', 'hashes')^0 * Cg(V'Multiline', 'multiline')^0), 66 | Scenario = Ct( 67 | V'Tags' * 68 | space^0 * "Scenario:" * space * V'Name' * newline * 69 | Cg(Ct(V'Step'^0), 'steps')) 70 | } 71 | 72 | function parse(feature_string) 73 | return lpeg.match(G, feature_string .. '\n') 74 | end 75 | 76 | local wildcard = P'(.*)' + '"(.*)"' + "'(.*)'" 77 | local non_wildcard = (1 - wildcard)^1 78 | local step_patt = Ct(C(non_wildcard + wildcard)^1) 79 | 80 | local wildcard_nospace = C((1 - space)^1) 81 | local wildcard_single_q = P"'" * C((P"\\'" + (1 - P"'"))^0) * "'" 82 | local wildcard_double_q = P'"' * C((P'\\"' + (1 - P'"'))^0) * '"' 83 | 84 | function make_step_pattern(name) 85 | local parts = lpeg.match(step_patt, name) 86 | local patt = P'' 87 | for _, part in pairs(parts) do 88 | if part == '(.*)' then 89 | patt = patt * wildcard_nospace 90 | elseif part == '"(.*)"' then 91 | patt = patt * wildcard_double_q 92 | elseif part == "'(.*)'" then 93 | patt = patt * wildcard_single_q 94 | else 95 | patt = patt * P(part) 96 | end 97 | end 98 | return Ct((1 - patt)^0 * patt) 99 | end 100 | 101 | function load_steps(path) 102 | local env = getfenv() 103 | local steps = {} 104 | 105 | for k, v in pairs(telescope.assertions) do 106 | setfenv(v, env) 107 | env[k] = v 108 | end 109 | setmetatable(env, {__index = _G}) 110 | 111 | local function step(name, fn) 112 | local patt = make_step_pattern(name) 113 | steps[patt] = fn 114 | end 115 | 116 | env.step = step 117 | local func, err = assert(loadfile(path)) 118 | if err then error(err) end 119 | setfenv(func, env)() 120 | 121 | return steps 122 | end 123 | 124 | function make_step(step, steps) 125 | for patt, step_fn in pairs(steps) do 126 | local params = lpeg.match(patt, step.name) 127 | if params then 128 | table.insert(params, 1, step) 129 | return function() step_fn(unpack(params)) end 130 | end 131 | end 132 | error('No matching step definition for "' .. step.name .. '"!') 133 | end 134 | 135 | function generate_contexts(feature_str, steps, contexts) 136 | local feature = parse(feature_str) 137 | 138 | contexts = contexts or {} -- telescope contexts generated 139 | local ctx = {} -- internal context handed to each test 140 | local current_scenario = 0 141 | 142 | -- telescope context format: 143 | -- for features: {context = true, name = "Feature name", parent = 0} 144 | -- for scenarios: {context = true, name = "Scenario name", parent = 1} 145 | -- for steps: {context_name = "Scenario name", name = "Step name", parent = parent_index, test = function...} 146 | 147 | table.insert(contexts, {context = true, name = feature.name, parent = 0}) 148 | for _, scenario in pairs(feature.scenarios) do 149 | table.insert(contexts, {context = true, name = scenario.name, parent = 1}) 150 | current_scenario = #contexts 151 | for _, step in pairs(scenario.steps) do 152 | step.context = ctx 153 | local step_fn = make_step(step, steps) 154 | table.insert(contexts, {context_name = contexts[current_scenario].name, 155 | name = step.name, parent = current_scenario, test = step_fn}) 156 | end 157 | end 158 | return contexts 159 | end -------------------------------------------------------------------------------- /cbsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | package.path = './?.lua;' .. package.path 3 | pcall(require, 'luarocks.require') 4 | require 'telescope' 5 | require 'calabash' 6 | require 'lfs' 7 | 8 | local steps = calabash.load_steps('features/steps.lua') 9 | 10 | 11 | local contexts = {} 12 | for filename in lfs.dir('features') do 13 | if string.match(filename, '.*%.feature') then 14 | local feature_str = io.open('features/' .. filename):read('*all') 15 | calabash.generate_contexts(feature_str, steps, contexts) 16 | end 17 | end 18 | 19 | local buffer = {} 20 | local results = telescope.run(contexts, callbacks, test_pattern) 21 | local summary, data = telescope.summary_report(contexts, results) 22 | 23 | table.insert(buffer, telescope.test_report(contexts, results)) 24 | 25 | table.insert(buffer, summary) 26 | local report = telescope.error_report(contexts, results) 27 | if report then 28 | table.insert(buffer, "") 29 | table.insert(buffer, report) 30 | end 31 | 32 | if #buffer > 0 then print(table.concat(buffer, "\n")) end 33 | 34 | for _, v in pairs(results) do 35 | if v.status_code == telescope.status_codes.err or 36 | v.status_code == telescope.status_codes.fail then 37 | os.exit(1) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /features/generator.feature: -------------------------------------------------------------------------------- 1 | Feature: Generate telescope contexts 2 | In order to test features 3 | As a test framework author 4 | I need to generate telescope contexts 5 | 6 | Scenario: Match on a simple string 7 | Given a step named "Hello there" 8 | When I make a step with name "Hello there" 9 | And I call that step function 10 | Then that step should be called with no parameters 11 | 12 | Scenario: Match on a non-whitespace wildcard 13 | Given a step named "Hello (.*) there" 14 | When I make a step with name "Hello you there" 15 | And I call that step function 16 | Then that step should be called with the parameter "you" 17 | 18 | Scenario: Match on multiple wildcards 19 | Given a step named 'Hello "(.*)" over (.*)' 20 | When I make a step with name 'Hello "you people" over there' 21 | And I call that step function 22 | Then that step should be called with these parameters: 23 | | param | 24 | | you people | 25 | | there | 26 | 27 | Scenario: Telescope steps for a simple feature 28 | Given a feature string: 29 | """ 30 | Feature: context 31 | 32 | Scenario: subcontext 33 | Given a test 34 | """ 35 | And a step named "a test" 36 | When I generate a telescope context 37 | Then that context should have the following values: 38 | | context | context_name | name | parent | 39 | | true | | context | 0 | 40 | | true | | subcontext | 1 | 41 | | | subcontext | Given a test | 2 | 42 | -------------------------------------------------------------------------------- /features/parser.feature: -------------------------------------------------------------------------------- 1 | Feature: Parse features 2 | In order to test features 3 | As a test framework author 4 | I need to parse features 5 | 6 | Scenario: Parse a feature name 7 | Given a feature string "Feature: Parse a feature name" 8 | When I parse the feature string 9 | Then I should get the following attributes: 10 | | key | value | 11 | | name | Parse a feature name | 12 | 13 | Scenario: Parse a feature name and description 14 | Given a feature string: 15 | """ 16 | Feature: Parse a feature name and description 17 | In order to... 18 | As a... 19 | I want to... 20 | 21 | """ 22 | When I parse the feature string 23 | The feature attribute "name" should be "Parse a feature name and description" 24 | And the feature attribute "description" should be: 25 | """ 26 | In order to... 27 | As a... 28 | I want to... 29 | """ 30 | 31 | Scenario: Parse a feature name, description, and scenario 32 | Given a feature string: 33 | """ 34 | Feature: Meta-parse a feature 35 | ... 36 | 37 | Scenario: Meta-scenario 38 | Given a Foo 39 | When I bar that foo 40 | I should see a foo'd bar 41 | """ 42 | When I parse the feature string 43 | The feature attribute "name" should be "Meta-parse a feature" 44 | And the feature attribute "description" should be "..." 45 | And the scenarios list should have length 1 46 | And scenario 1 attribute "name" should be "Meta-scenario" 47 | And scenario 1 should have these steps: 48 | | step | 49 | | Given a Foo | 50 | | When I bar that foo | 51 | | I should see a foo'd bar | 52 | 53 | Scenario: Parse a feature and scenario with a table in the steps 54 | Given a feature string: 55 | """ 56 | Feature: with table 57 | ... 58 | 59 | Scenario: with table 60 | Given the table: 61 | | a | b | 62 | | 1 | 2 | 63 | | 3 | 4 | 64 | | 5 | | 65 | """ 66 | When I parse the feature string 67 | Then scenario 1 step 1 row 1 field "a" should be "1" 68 | And scenario 1 step 1 row 1 field "b" should be "2" 69 | And scenario 1 step 1 row 2 field "a" should be "3" 70 | And scenario 1 step 1 row 2 field "b" should be "4" 71 | And scenario 1 step 1 row 3 field "a" should be "5" 72 | And scenario 1 step 1 row 3 field "b" should be "" 73 | 74 | Scenario: Parse a feature and scenario with double-quoted long strings 75 | Given a feature string: 76 | ''' 77 | Feature: with long string 78 | ... 79 | 80 | Scenario: with long string 81 | Given the long string: 82 | """ 83 | one 84 | two 85 | three 86 | """ 87 | ''' 88 | When I parse the feature string 89 | Then scenario 1 step 1 multiline line 1 should be "one" 90 | And scenario 1 step 1 multiline line 2 should be " two" 91 | And scenario 1 step 1 multiline line 3 should be " three" 92 | 93 | Scenario: Parse a feature and scenario with single-quoted long strings 94 | Given a feature string: 95 | """ 96 | Feature: with long string 97 | ... 98 | 99 | Scenario: with long string 100 | Given the long string: 101 | ''' 102 | four 103 | five 104 | six 105 | ''' 106 | """ 107 | When I parse the feature string 108 | Then scenario 1 step 1 multiline line 1 should be "four" 109 | And scenario 1 step 1 multiline line 2 should be "five" 110 | And scenario 1 step 1 multiline line 3 should be "six" 111 | 112 | Scenario: Parse single tags in features 113 | Given a feature string: 114 | """ 115 | @tag1 116 | Feature: tags 117 | """ 118 | When I parse the feature string 119 | Then the feature should have the tags: 120 | | tag | 121 | | tag1 | 122 | 123 | Scenario: Parse multiple tags in features 124 | Given a feature string: 125 | """ 126 | @tag1 @tag2 127 | @tag3 128 | @tag4 @tag5 129 | Feature: tags 130 | """ 131 | When I parse the feature string 132 | Then the feature should have the tags: 133 | | tag | 134 | | tag1 | 135 | | tag2 | 136 | | tag3 | 137 | | tag4 | 138 | | tag5 | 139 | 140 | Scenario: Parse tags in scenarios 141 | Given a feature string: 142 | """ 143 | Feature: tags in scenarios 144 | 145 | @tagA @tagB @tagC 146 | @tagD 147 | Scenario: I gots tags! 148 | Step 1 149 | Step 2 150 | Step 3 151 | """ 152 | When I parse the feature string 153 | Then scenario 1 should have the tags: 154 | | tag | 155 | | tagA | 156 | | tagB | 157 | | tagC | 158 | | tagD | 159 | And scenario 1 should have these steps: 160 | | step | 161 | | Step 1 | 162 | | Step 2 | 163 | | Step 3 | 164 | -------------------------------------------------------------------------------- /features/steps.lua: -------------------------------------------------------------------------------- 1 | require 'calabash' 2 | require 'lpeg' 3 | 4 | step('a feature string "(.*)"', 5 | function(step, str) 6 | step.context.feature_string = str 7 | end) 8 | 9 | step('a feature string:', 10 | function(step) 11 | step.context.feature_string = step.multiline 12 | end) 13 | 14 | step('parse the feature string', 15 | function(step) 16 | step.context.feature = calabash.parse(step.context.feature_string) 17 | end) 18 | 19 | step('I should get the following attributes:', 20 | function(step) 21 | local feature = step.context.feature 22 | for i = 1, #step.hashes do 23 | local hash = step.hashes[i] 24 | assert_equal(feature[hash.key], hash.value) 25 | end 26 | end) 27 | 28 | step('scenario (.*) attribute "(.*)" should be "(.*)"', 29 | function(step, num, name, value) 30 | assert_equal(step.context.feature.scenarios[tonumber(num)][name], value) 31 | end) 32 | 33 | step('feature attribute "(.*)" should be "(.*)"', 34 | function(step, name, value) 35 | assert_equal(step.context.feature[name], value) 36 | end) 37 | 38 | step('feature attribute "(.*)" should be:', 39 | function(step, name) 40 | assert_equal(step.context.feature[name], step.multiline) 41 | end) 42 | 43 | step('scenarios list should have length (.*)', 44 | function(step, length) 45 | assert_equal(#step.context.feature.scenarios, tonumber(length)) 46 | end) 47 | 48 | step('scenario (.*) should have these steps:', 49 | function(step, num) 50 | local actual_steps = step.context.feature.scenarios[tonumber(num)].steps 51 | for i, step in ipairs(step.hashes) do 52 | assert_equal(actual_steps[i].name, step.step) 53 | end 54 | end) 55 | 56 | step('scenario (.*) step (.*) row (.*) field "(.*)" should be "(.*)"', 57 | function(step, scenario, step_n, row, field, expected_value) 58 | scenario, step_n, row = tonumber(scenario), tonumber(step_n), tonumber(row) 59 | local actual_value = step.context.feature.scenarios[scenario].steps[step_n].hashes[row][field] 60 | assert_equal(actual_value, expected_value) 61 | end) 62 | 63 | function split (s, sep) 64 | sep = lpeg.P(sep) 65 | local elem = lpeg.C((1 - sep)^0) 66 | local p = lpeg.Ct(elem * (sep * elem)^0) 67 | return lpeg.match(p, s) 68 | end 69 | 70 | step('scenario (.*) step (.*) multiline line (.*) should be "(.*)"', 71 | function(step, scenario, step_n, line_n, expected_value) 72 | scenario, step_n, line_n = tonumber(scenario), tonumber(step_n), tonumber(line_n) 73 | local lines = split(step.context.feature.scenarios[scenario].steps[step_n].multiline, '\n') 74 | assert_equal(lines[line_n], expected_value) 75 | end) 76 | 77 | function a_step_named(step, name) 78 | step.context.steps = step.context.steps or {} 79 | step.context.steps[calabash.make_step_pattern(name)] = function(step_, ...) 80 | step.context.step_params = {...} 81 | end 82 | end 83 | step('a step named "(.*)"', a_step_named) 84 | step("a step named '(.*)'", a_step_named) 85 | 86 | function make_a_step_with_name(step, name) 87 | step.context.created_step = calabash.make_step({name = name}, step.context.steps) 88 | end 89 | step('make a step with name "(.*)"', make_a_step_with_name) 90 | step("make a step with name '(.*)'", make_a_step_with_name) 91 | 92 | step('call that step', 93 | function(step) 94 | step.context.created_step() 95 | end) 96 | 97 | step('step should be called with no parameters', 98 | function(step) 99 | assert_equal(#step.context.step_params, 0) 100 | end) 101 | 102 | step('step should be called with the parameter "(.*)"', 103 | function(step, param) 104 | assert_equal(#step.context.step_params, 1) 105 | assert_equal(step.context.step_params[1], param) 106 | end) 107 | 108 | step('step should be called with these parameters:', 109 | function(step) 110 | for i, hash in pairs(step.hashes) do 111 | assert_equal(step.context.step_params[i], hash.param) 112 | end 113 | end) 114 | 115 | step('generate a telescope context', 116 | function(step) 117 | step.context.telescope_context = calabash.generate_contexts(step.context.feature_string, 118 | step.context.steps) 119 | end) 120 | 121 | step('context should have the following values:', 122 | function(step) 123 | for i, context in pairs(step.context.telescope_context) do 124 | local expected = step.hashes[i] 125 | for key, value in pairs(context) do 126 | if key ~= 'test' then 127 | assert_equal(tostring(value), expected[key]) 128 | end 129 | end 130 | end 131 | end) 132 | 133 | step('feature should have the tags:', 134 | function(step) 135 | for i, tag in pairs(step.hashes) do 136 | assert_equal(step.context.feature.tags[i], tag.tag) 137 | end 138 | end) 139 | 140 | step('scenario (.*) should have the tags:', 141 | function(step, scenario) 142 | local tags = step.context.feature.scenarios[tonumber(scenario)].tags 143 | for i, tag in pairs(step.hashes) do 144 | assert_equal(tags[i], tag.tag) 145 | end 146 | end) -------------------------------------------------------------------------------- /rockspecs/calabash-0.1.1-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'calabash' 2 | version = '0.1.0-1' 3 | source = { 4 | url = 'https://github.com/downloads/pib/calabash/calabash-0.1.1.tar.gz', 5 | md5 = 'e4c35714ac83c71e0157af0f3d77036e' 6 | } 7 | description = { 8 | summary = 'A cucumber-like BDD library on top of telescope.', 9 | detailed = [[ 10 | Calabash is a domain-specific language for behavior-driven-development, 11 | modelled after Cucumber (http://cukes.info/). 12 | ]], 13 | license = 'MIT/X11', 14 | homepage = 'http://github.com/pib/calabash' 15 | } 16 | dependencies = { 17 | 'lua >= 5.1', 18 | 'telescope >= 0.4', 19 | 'lpeg >= 0.10', 20 | 'luafilesystem >= 1.5' 21 | } 22 | 23 | build = { 24 | type = 'none', 25 | install = { 26 | lua = { 27 | 'calabash.lua' 28 | }, 29 | bin = { 30 | 'cbsh' 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /rockspecs/calabash-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'calabash' 2 | version = 'scm-1' 3 | source = { 4 | url = 'git://github.com/pib/calabash.git' 5 | } 6 | description = { 7 | summary = 'A cucumber-like BDD library on top of telescope.', 8 | detailed = [[ 9 | Calabash is a domain-specific language for behavior-driven-development, 10 | modelled after Cucumber (http://cukes.info/). 11 | ]], 12 | license = 'MIT/X11', 13 | homepage = 'http://github.com/pib/calabash' 14 | } 15 | dependencies = { 16 | 'lua >= 5.1', 17 | 'telescope >= 0.4', 18 | 'lpeg >= 0.10', 19 | 'luafilesystem >= 1.5' 20 | } 21 | 22 | build = { 23 | type = 'none', 24 | install = { 25 | lua = { 26 | 'calabash.lua' 27 | }, 28 | bin = { 29 | 'cbsh' 30 | } 31 | } 32 | } --------------------------------------------------------------------------------