cellophane 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ Feature: Step Definitions
2
+
3
+ @1
4
+ Scenario: Requiring existing step definition
5
+
6
+ Given a project directory with the following structure
7
+ | type | path |
8
+ | directory | features |
9
+ | directory | features/step_definitions |
10
+ | file | features/one.feature |
11
+ | file | features/two.feature |
12
+ | file | features/step_definitions/one_steps.rb |
13
+ When Cellophane is called with "one"
14
+ Then the command should include "features/one.feature"
15
+ And the command should include "-r features/step_definitions/one_steps.rb"
16
+
17
+ @2
18
+ Scenario: Not requiring step definition that doesn't exist
19
+
20
+ Given a project directory with the following structure
21
+ | type | path |
22
+ | directory | features |
23
+ | directory | features/step_definitions |
24
+ | file | features/one.feature |
25
+ | file | features/two.feature |
26
+ | file | features/step_definitions/one_steps.rb |
27
+ When Cellophane is called with "two"
28
+ Then the command should include "features/two.feature"
29
+ And the command should not include "-r features/step_definitions/two_steps.rb"
30
+
31
+ @3
32
+ Scenario: Step definitions nested in feature subdirectories
33
+
34
+ Given a project directory with the following structure
35
+ | type | path |
36
+ | directory | features |
37
+ | directory | features/admin |
38
+ | directory | features/admin/step_definitions |
39
+ | file | features/admin/one.feature |
40
+ | file | features/admin/two.feature |
41
+ | file | features/admin/step_definitions/one_steps.rb |
42
+ And a project options file with the following options
43
+ | option |
44
+ | step_path: {nested_in: step_definitions} |
45
+ When Cellophane is called with "admin/one"
46
+ Then the command should include "features/admin/one.feature"
47
+ And the command should include "-r features/admin/step_definitions/one_steps.rb"
@@ -0,0 +1,29 @@
1
+ module CellophaneMethods
2
+ def call_cellophane(args = [])
3
+ cellophane = Cellophane::Main.new(args)
4
+ @command = cellophane.command
5
+ @message = cellophane.message
6
+ end
7
+
8
+ def output_command
9
+ puts "\n\n#{@command}\n\n"
10
+ end
11
+
12
+ def output_message
13
+ puts "\n\n#{@message}\n\n"
14
+ end
15
+
16
+ def save_initial_dir
17
+ @initial_dir = Dir.pwd
18
+ end
19
+
20
+ def restore_initial_dir
21
+ Dir.chdir(@initial_dir)
22
+ end
23
+
24
+ def ensure_project_dir_removed
25
+ FileUtils.remove_dir(@project_dir, true) if @project_dir && File.exist?(@project_dir)
26
+ end
27
+ end
28
+
29
+ World(CellophaneMethods)
@@ -0,0 +1,43 @@
1
+ Given /^I debug$/ do
2
+ require 'ruby-debug'
3
+ debugger
4
+ puts "\n\ndebugging\n\n"
5
+ end
6
+
7
+ Given /^Cellophane is called with "([^"]*)"$/ do |args|
8
+ call_cellophane(args.split(' '))
9
+ end
10
+
11
+ Given /^the (command|message) should include "([^"]+)"$/ do |what, expected|
12
+ (what == 'command' ? @command : @message).should =~ /#{expected}/
13
+ end
14
+
15
+ Given /^the (command|message) should not include "([^"]+)"$/ do |what, expected|
16
+ (what == 'command' ? @command : @message).should_not =~ /#{expected}/
17
+ end
18
+
19
+ Given /^a project directory with the following structure$/ do |structure|
20
+ @project_dir = './test_project'
21
+ # just in case the last run failed to exit cleanly, delete the test_project directory
22
+ # if it exists
23
+ FileUtils.remove_dir(@project_dir, true) if File.exist?(@project_dir)
24
+ FileUtils.mkdir(@project_dir)
25
+ Dir.chdir(@project_dir)
26
+
27
+ structure.hashes.each do |entry|
28
+ if entry[:type] == 'directory'
29
+ FileUtils.mkdir_p(entry[:path])
30
+ else
31
+ File.open(entry[:path], 'w') {|f| f.write("# #{entry[:path]}") }
32
+ end
33
+ end
34
+ end
35
+
36
+ Given /^a project options file with the following options$/ do |lines|
37
+ options = lines.hashes.collect { |line| line[:option] }
38
+
39
+ contents = options.join("\n")
40
+
41
+ # already in the project dir
42
+ File.open(Cellophane::PROJECT_OPTIONS_FILE, 'w') { |f| f.write(contents) }
43
+ end
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
+ require 'cellophane/main'
3
+ require 'rspec/expectations'
4
+
5
+ Before do
6
+ save_initial_dir
7
+ ensure_project_dir_removed
8
+ end
9
+
10
+ Before('@debug') do
11
+ require 'ruby-debug'
12
+ end
13
+
14
+ After do
15
+ restore_initial_dir
16
+ ensure_project_dir_removed
17
+ end
@@ -0,0 +1,55 @@
1
+ Feature: Tags
2
+
3
+ @1
4
+ Scenario: OR tag
5
+
6
+ When Cellophane is called with "-t one,two"
7
+ Then the command should include "-t @one,@two"
8
+
9
+ @2
10
+ Scenario: NOT tag
11
+
12
+ When Cellophane is called with "-t one,~two"
13
+ Then the command should include "-t @one -t ~@two"
14
+
15
+ @3
16
+ Scenario: AND tag
17
+
18
+ When Cellophane is called with "-t one,+two"
19
+ Then the command should include "-t @one -t @two"
20
+
21
+ @4
22
+ Scenario: Mixed tags in a logical order
23
+
24
+ When Cellophane is called with "-t one,two,~three,+four"
25
+ Then the command should include "-t @one,@two -t @four -t ~@three"
26
+
27
+ @5
28
+ Scenario: Mixed tags not in a logical order
29
+
30
+ When Cellophane is called with "-t +four,one,~three,two"
31
+ Then the command should include "-t @one,@two -t @four -t ~@three"
32
+
33
+ @6
34
+ Scenario: Numeric OR tag ranges
35
+
36
+ When Cellophane is called with "-t 1-3"
37
+ Then the command should include "-t @1,@2,@3"
38
+
39
+ @7
40
+ Scenario: Numeric NOT tag ranges
41
+
42
+ When Cellophane is called with "-t ~1-3"
43
+ Then the command should include "-t ~@1 -t ~@2 -t ~@3"
44
+
45
+ @8
46
+ Scenario: Numeric OR tag range with a NOT in the OR range
47
+
48
+ When Cellophane is called with "-t 1-3,~2"
49
+ Then the command should include "-t @1,@3"
50
+
51
+ @9
52
+ Scenario: Numeric OR tag range with a NOT out of the OR range
53
+
54
+ When Cellophane is called with "-t 1-3,~slow"
55
+ Then the command should include "-t @1,@2,@3 -t ~@slow"
@@ -0,0 +1,82 @@
1
+ require 'fileutils'
2
+ require 'cellophane/parser'
3
+ require 'cellophane/options'
4
+
5
+ module Cellophane
6
+
7
+ PROJECT_OPTIONS_FILE = '.cellophane.yaml'
8
+
9
+ class Main
10
+ attr_reader :command, :message, :project_options_file
11
+
12
+ def initialize(args = nil)
13
+ args ||= ARGV
14
+ @project_options_file = Cellophane::PROJECT_OPTIONS_FILE
15
+ @options = Cellophane::Options.parse(args)
16
+
17
+ @message = 'Invalid regular expression provided.' and return if @options[:regexp] && @options[:pattern].nil?
18
+
19
+ parser = Cellophane::Parser.new(@options)
20
+ @features = parser.features
21
+
22
+ @message = 'No features matching PATTERN were found.' and return unless @features
23
+
24
+ @tags = parser.tags
25
+
26
+ @command = generate_command
27
+ end
28
+
29
+ def run
30
+ puts @message and return if @message
31
+ @options[:print] ? puts(@command) : system("#{@command}\n\n")
32
+ end
33
+
34
+ private
35
+
36
+ def generate_command
37
+ cuke_cmd = "cucumber #{@options[:cucumber]}"
38
+
39
+ features = []
40
+ steps = []
41
+
42
+ if @features.any?
43
+ @features.each do |file|
44
+ file_parts = split_feature(file)
45
+ features << construct_feature_file(file_parts[:path], file_parts[:name])
46
+ steps << construct_step_file(file_parts[:path], file_parts[:name])
47
+ end
48
+
49
+ else
50
+ # if there are no features explicitly identified, then cucumber will run all. However,
51
+ # if we are using non-standard locations for features or step definitions, we must tell
52
+ # cucumber accordingly
53
+ features << @options[:feature_path] if @options[:non_standard_feature_path]
54
+ steps << @options[:step_path] if @options[:non_standard_step_path]
55
+ end
56
+
57
+ requires = (@options[:requires] + steps).compact.uniq
58
+ cuke_cmd += " -r #{requires.join(' -r ')}" if requires.any?
59
+ cuke_cmd += " #{features.join(' ')}" if features.any?
60
+ return "#{cuke_cmd} #{@tags}".gsub(' ', ' ')
61
+ end
62
+
63
+ def construct_feature_file(path, file)
64
+ "#{@options[:feature_path]}/#{path}/#{file}.feature".gsub('//', '/')
65
+ end
66
+
67
+ def construct_step_file(path, file)
68
+ step_path = @options[:step_path].is_a?(Hash) ? "#{@options[:feature_path]}/#{path}/#{@options[:step_path][:nested_in]}" : "#{@options[:step_path]}/#{path}"
69
+ step_file = "#{step_path}/#{file}_steps.rb".gsub('//', '/')
70
+ return File.exist?(step_file) ? step_file : nil
71
+ end
72
+
73
+ def split_feature(file)
74
+ name = File.basename(file, '.feature')
75
+ # now get rid of the file_name and the feature_path
76
+ path = File.dirname(file).gsub(@options[:feature_path_regexp], '')
77
+ return {:path => path, :name => name}
78
+ end
79
+
80
+ end # class Main
81
+ end # module Cellophane
82
+
@@ -0,0 +1,145 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+
4
+ module Cellophane
5
+ class Options
6
+ def self.parse(args)
7
+ default_options = self.get_options(:default)
8
+ project_options = self.get_options(:project)
9
+ merged_options = default_options.merge(project_options)
10
+
11
+ option_parser = OptionParser.new do |opts|
12
+ # Set a banner, displayed at the top of the help screen.
13
+ # TODO add example usage including ~feature, patterns, and ~tag
14
+ opts.banner = "Usage: cellophane [options] PATTERN"
15
+
16
+ opts.on('-r', '--regexp', 'PATTERN is a regular expression. Default is false.') do
17
+ merged_options[:regexp] = true
18
+ end
19
+
20
+ opts.on('-t', '--tags TAGS', 'Tags to include/exclude.') do |tags|
21
+ merged_options[:tags] = tags
22
+ end
23
+
24
+ opts.on('-c', '--cucumber OPTIONS', 'Options to pass to cucumber.') do |cucumber|
25
+ merged_options[:cucumber] = cucumber
26
+ end
27
+
28
+ opts.on('-p', '--print', 'Echo the command instead of calling cucumber.') do
29
+ merged_options[:print] = true
30
+ end
31
+
32
+ opts.on('-d', '--debug', 'Require ruby-debug.') do
33
+ require 'rubygems'
34
+ require 'ruby-debug'
35
+ end
36
+
37
+ # This displays the help screen, all programs are assumed to have this option.
38
+ opts.on( '-h', '--help', 'Display this screen.' ) do
39
+ puts opts
40
+ exit(0)
41
+ end
42
+ end
43
+
44
+ option_parser.parse!(args)
45
+
46
+ # get the pattern from the command line (no switch)
47
+ merged_options[:pattern] = args.first if args.any?
48
+
49
+ return self.normalize_options(merged_options)
50
+ end # self.parse
51
+
52
+ private
53
+
54
+ def self.get_options(type = :default)
55
+ if type == :project
56
+ project_options = {}
57
+
58
+ # load is used here due to require not requiring a file if
59
+ # it has already been required. This is mainly for testing
60
+ # purposes (multiple features needing to have different
61
+ # options for validation), but it shouldn't make a difference
62
+ # for run time.
63
+ #load project_options_file if File.exist?(project_options_file)
64
+
65
+ if File.exist?(Cellophane::PROJECT_OPTIONS_FILE)
66
+ yaml_options = YAML.load_file(Cellophane::PROJECT_OPTIONS_FILE)
67
+
68
+ ['cucumber', 'feature_path', 'feature_path_regexp', 'step_path', 'requires'].each do |key|
69
+ project_options[key.to_sym] = yaml_options[key] if yaml_options.has_key?(key)
70
+ end
71
+ end
72
+
73
+ project_options
74
+ else
75
+ {
76
+ :pattern => nil,
77
+ :regexp => false,
78
+ :print => false,
79
+ :cucumber => nil,
80
+ :tags => nil,
81
+ :feature_path => 'features',
82
+ :feature_path_regexp => nil,
83
+ :step_path => 'features/step_definitions',
84
+ :requires => []
85
+ }
86
+ end
87
+ end # get_options
88
+
89
+ def self.normalize_options(options)
90
+ defaults = self.get_options(:default)
91
+
92
+ # ran into freezing problems in Ruby 1.9.2 otherwise
93
+ tmp_options = options.dup
94
+
95
+ # normalize the paths for features and steps
96
+ # had originally used the gsub! and sub!, but hit freezing problems with Ruby 1.9.2
97
+
98
+ # globs don't work with backslashes (if on windows)
99
+ tmp_options[:feature_path] = tmp_options[:feature_path].gsub(/\\/, '/')
100
+ # strip trailing slash
101
+ tmp_options[:feature_path] = tmp_options[:feature_path].sub(/\/$/, '')
102
+
103
+ if tmp_options[:step_path].is_a?(Hash)
104
+ # if the step path is configured in YAML, it will be a string key, not a symbol
105
+ key = tmp_options[:step_path].has_key?('nested_in') ? 'nested_in' : :nested_in
106
+ if tmp_options[:step_path].has_key?(key)
107
+ tmp_options[:step_path][:nested_in] = tmp_options[:step_path][key].gsub(/\\/, '/')
108
+ tmp_options[:step_path][:nested_in] = tmp_options[:step_path][key].sub(/\/$/, '')
109
+ end
110
+ else
111
+ tmp_options[:step_path] = tmp_options[:step_path].gsub(/\\/, '/')
112
+ tmp_options[:step_path] = tmp_options[:step_path].sub(/\/$/, '')
113
+ end
114
+
115
+ # need to know this later
116
+ tmp_options[:non_standard_feature_path] = tmp_options[:feature_path] != defaults[:feature_path]
117
+ tmp_options[:non_standard_step_path] = tmp_options[:step_path] != defaults[:step_path]
118
+
119
+ # make a regexp out of the features path if there isn't one already. we need to escape slashes so the
120
+ # regexp can be made
121
+ tmp_options[:feature_path_regexp] = Regexp.new(tmp_options[:feature_path].gsub('/', '\/')) unless tmp_options[:feature_path_regexp]
122
+
123
+ # just in case someone sets necessary values to nil, let's go back to defaults
124
+ tmp_options[:regexp] ||= defaults[:regexp]
125
+ tmp_options[:feature_path] ||= defaults[:feature_path]
126
+ tmp_options[:step_path] ||= defaults[:step_path]
127
+ tmp_options[:requires] ||= defaults[:requires]
128
+
129
+ # do what needs to be done on the pattern
130
+ unless tmp_options[:pattern].nil?
131
+ tmp_options[:pattern] = tmp_options[:pattern].strip
132
+ tmp_options[:pattern] = nil unless tmp_options[:pattern] && !tmp_options[:pattern].empty?
133
+
134
+ begin
135
+ tmp_options[:pattern] = Regexp.new(tmp_options[:pattern]) if tmp_options[:regexp]
136
+ rescue
137
+ # if the regexp fails for some reason
138
+ tmp_options[:pattern] = nil
139
+ end
140
+ end
141
+
142
+ return tmp_options
143
+ end # normalize_options
144
+ end # class Options
145
+ end
@@ -0,0 +1,137 @@
1
+ module Cellophane
2
+ class Parser
3
+ def initialize(options)
4
+ @options = options
5
+ end
6
+
7
+ def features
8
+ # if no pattern is specified, let cucumber run 'em all
9
+ return [] if @options[:pattern].nil?
10
+ collected_features = @options[:regexp] ? collect_features_by_regexp : collect_features_by_glob
11
+ return collected_features.any? ? collected_features : nil
12
+ end # features
13
+
14
+ def tags
15
+ tags = {
16
+ :or => [],
17
+ :and => [],
18
+ :not => []
19
+ }
20
+
21
+ return '' if @options[:tags].nil?
22
+
23
+ @options[:tags].split(',').each do |t|
24
+ # if tags are numeric, let's support ranges !!!
25
+ if t =~ /^(~)?([0-9]+)-([0-9]+)$/
26
+ x = $2.to_i
27
+ y = $3.to_i
28
+ exclude = $1
29
+
30
+ # in case the user put them in the wrong order ... doh!
31
+ if x > y
32
+ z = x.dup
33
+ x = y.dup
34
+ y = z.dup
35
+ end
36
+
37
+ (x..y).each do |i|
38
+ if exclude
39
+ tags[:not] << "#{i}"
40
+ else
41
+ tags[:or] << "#{i}"
42
+ end
43
+ end
44
+ else
45
+ if t =~ /^~(.+)/
46
+ tags[:not] << $1
47
+ elsif t =~ /^\+(.+)/
48
+ tags[:and] << $1
49
+ else
50
+ tags[:or] << t
51
+ end
52
+ end
53
+ end # each
54
+
55
+ [:and, :or, :not].each { |type| tags[type].uniq! }
56
+
57
+ # if there are AND/OR tags, remove any NOT tags so we avoid
58
+ # duplicating the tag when passing to cucumber...so instead of
59
+ # cucumber -t @1,@2,@3 -t ~@2
60
+ # we'd like to see
61
+ # cucumber -t @1,@3
62
+
63
+ intersection = tags[:or] & tags[:not]
64
+ tags[:or] -= intersection
65
+ tags[:not] -= intersection
66
+
67
+ intersection = tags[:and] & tags[:not]
68
+ tags[:and] -= intersection
69
+ tags[:not] -= intersection
70
+
71
+ # now add @ and ~ as appropriate
72
+ tags[:or].each_with_index { |tag, i| tags[:or][i] = "@#{tag}" }
73
+ tags[:and].each_with_index { |tag, i| tags[:and][i] = "@#{tag}" }
74
+ tags[:not].each_with_index { |tag, i| tags[:not][i] = "~@#{tag}" }
75
+
76
+ tags_fragment = ''
77
+ tags_fragment += "-t #{tags[:or].join(',')} " if tags[:or].any?
78
+ tags_fragment += "-t #{tags[:and].join(' -t ')} " if tags[:and].any?
79
+ tags_fragment += "-t #{tags[:not].join(' -t ')}" if tags[:not].any?
80
+
81
+ # if the user passes in tags with @ already in it
82
+ tags_fragment.gsub('@@', '@')
83
+ end # def self.parse_tags
84
+
85
+ private
86
+
87
+ def collect_features_by_regexp
88
+ features = []
89
+
90
+ # start by globbing all feature files
91
+ Dir.glob("#{@options[:feature_path]}/**/*.feature").each do |feature_file|
92
+ # keep the ones that match the regexp
93
+ features << feature_file if @options[:pattern].match(feature_file)
94
+ end
95
+
96
+ features.uniq
97
+ end # collect_features_by_regexp
98
+
99
+ def collect_features_by_glob
100
+ only = []
101
+ except = []
102
+ features_to_include = []
103
+ features_to_exclude = []
104
+ pattern = @options[:pattern].dup
105
+
106
+ # want to run certain ones and/or exclude certain ones
107
+ pattern.split(',').each do |f|
108
+ if f[0].chr == '~'
109
+ except << f[1..f.length]
110
+ else
111
+ only << f
112
+ end
113
+ end
114
+
115
+ # if we have an exception, we want to get all features by default
116
+ pattern = '**/*' if except.any?
117
+ # unless we specifically say we want only certain ones
118
+ pattern = nil if only.any?
119
+
120
+ if only.any?
121
+ only.each do |f|
122
+ features_to_include += Dir.glob("#{@options[:feature_path]}/#{f}.feature")
123
+ end
124
+ else
125
+ features_to_include += Dir.glob("#{@options[:feature_path]}/#{pattern}.feature")
126
+ end
127
+
128
+ if except.any?
129
+ except.each do |f|
130
+ features_to_exclude = Dir.glob("#{@options[:feature_path]}/#{f}.feature")
131
+ end
132
+ end
133
+
134
+ (features_to_include - features_to_exclude).uniq
135
+ end # collect_features_by_glob
136
+ end # class Parser
137
+ end # module Cellophane
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Cellophane" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'cellophane'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cellophane
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Phillip Koebbe
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-01-09 00:00:00 -06:00
18
+ default_executable: cellophane
19
+ dependencies: []
20
+
21
+ description: Cellophane is a thin wrapper around Cucumber, making it easier to be creative when running features.
22
+ email: phillip@livingdoor.net
23
+ executables:
24
+ - cellophane
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - LICENSE.txt
29
+ - README.textile
30
+ files:
31
+ - .cellophane.yaml
32
+ - .cellophane.yaml.sample
33
+ - .document
34
+ - LICENSE.txt
35
+ - README.textile
36
+ - Rakefile
37
+ - VERSION
38
+ - bin/cellophane
39
+ - features/feature_paths.feature
40
+ - features/glob_pattern.feature
41
+ - features/project_options.feature
42
+ - features/regular_expression_pattern.feature
43
+ - features/step_definitions.feature
44
+ - features/support/cellophane_methods.rb
45
+ - features/support/cellophane_steps.rb
46
+ - features/support/env.rb
47
+ - features/tags.feature
48
+ - lib/cellophane/main.rb
49
+ - lib/cellophane/options.rb
50
+ - lib/cellophane/parser.rb
51
+ - spec/cellophane_spec.rb
52
+ - spec/spec_helper.rb
53
+ has_rdoc: true
54
+ homepage: http://github.com/phillipkoebbe/cellophane
55
+ licenses:
56
+ - MIT
57
+ post_install_message:
58
+ rdoc_options: []
59
+
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.7
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: A thin wrapper around Cucumber.
85
+ test_files:
86
+ - spec/cellophane_spec.rb
87
+ - spec/spec_helper.rb