cellophane 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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