marso 0.1.15089 → 0.1.29007

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9dd68ffdf94aa8dc6c18a66a1be55774e8aa2b4b
4
- data.tar.gz: 07415303338d5c40520661aa865a656b171cb148
3
+ metadata.gz: 68643dc298346f9de6a9ab082f1a36186a233ee3
4
+ data.tar.gz: 95b856464d21e45e9c0e0739a888f6a145ab42d8
5
5
  SHA512:
6
- metadata.gz: 1386739d560795b6bcc2e6e74a59030aa46e9eeacfe3951bc3d62fbb58b9b48da18ea7ba2e7cb0374c151e9699e04ccc8595725e2e2ce38115034e529dad03db
7
- data.tar.gz: fab73a4f66f4856dc1f2500f0aeeb9693bd9240caa7f0ca12d9a8f6b35812635dff42fc7e7fabd4e88a006392f0277502fbff4917abc9a3c393dc9660b260f66
6
+ metadata.gz: 192be817ccbc445432761b2387d647d472cff3ac4742a5c3a3d41032f767200a71fde202c773ae9dc05e158539ee4cac102824195381eac975af69399f99a2ca
7
+ data.tar.gz: 7b42ebd3d40cad49f852e2fb2431c7ed8c27e79414ce1818979d50a1eda38096073737e8b8cf23908304668fe9f8d8b43d40c98110c3ce446fd2adb715ea519a
data/lib/marso.rb CHANGED
@@ -1,5 +1,20 @@
1
1
  require "marso/version"
2
- require "marso/assert"
3
2
  require "marso/factories"
4
3
  require "marso/config"
5
- require "marso/scenario"
4
+ require "marso/launcher"
5
+ require "marso/domain/step/step"
6
+ require "marso/domain/step/step_publish"
7
+ require "marso/domain/scenario/scenario"
8
+ require "marso/domain/scenario/scenario_publish"
9
+ require "marso/domain/story/story"
10
+ require "marso/domain/story/story_load"
11
+ require "marso/domain/story/story_publish"
12
+ require "marso/domain/feature/feature"
13
+ require "marso/domain/feature/feature_load"
14
+ require "marso/domain/feature/feature_publish"
15
+ require "marso/helpers/texthelper"
16
+ require "marso/helpers/statushelper"
17
+ require "marso/helpers/componenthelper"
18
+ require "marso/messages/errors"
19
+ require "marso/validation/symbol"
20
+ require "marso/toolbelt/fiberpiping"
data/lib/marso/config.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  module Marso
2
2
  class Config
3
3
  @@configuration={
4
- # If true, the result of each step is output to the console in realtime.
5
- # The consequence os setting that config to true is that steps
6
- # may be harder to read together when multiple scenarios are executed
7
- # paralell. Steps of different scenarios may indeed be intertwined in the
8
- # console
4
+ # If true, the result of each step is output to the console in realtime
5
+ # rather than waiting for the entire scenario to finish and then display
6
+ # all the steps all at once. Setting that config to true may make reading
7
+ # the output harder when multiple scenarios are executed in paralell.
8
+ # Steps of different scenarios may indeed be intertwined in the console
9
9
  :realtime_step_stdout => false,
10
10
 
11
11
  # If true, all steps of the same scenario defined after a broken step(i.e.
@@ -0,0 +1,124 @@
1
+ require 'securerandom'
2
+ require_relative 'feature_load'
3
+ require_relative 'feature_publish'
4
+ require_relative '../../helpers/statushelper'
5
+
6
+ module Marso
7
+
8
+ class Feature
9
+ include FeatureLoad
10
+ include FeaturePublish
11
+
12
+ attr_reader :id, :description, :status, :ctx, :stories, :scenario_contexts,
13
+ :rootpath, :header, :text, :tree_position, :color_theme
14
+
15
+ # description (optional): Hash defined as follow
16
+ # :id => Arbitrary number or string. Default is randomly generated
17
+ # (hex string (length 8))
18
+ # :name => Story's name
19
+ # :in_order_to => String that describes the fundamental story's business
20
+ # value
21
+ # :as_a => String that describes the user(s)
22
+ # :i => String that describes the feature that could deliver the story's
23
+ # business value(e.g. I want ... or I should ...)
24
+ # :stories => Array of all the stories that are part of that feature
25
+ # :scenario_contexts => Array of all the scenario contexts that are part
26
+ # of that feature
27
+ # :rootpath => Path to the folder that contain this current feature's file
28
+ # as well as its associated 'scenarios' and 'stories' folder
29
+ # :status => Can only be set if both :scenario_contexts and :stories are
30
+ # empty. Otherwise, it will be overidden by the status of
31
+ # either :scenario_contexts or :stories
32
+ def initialize(description={}, ctx={})
33
+ validate_arguments(description, ctx)
34
+
35
+ @description = description.clone
36
+ @ctx = ctx.clone
37
+
38
+ @description[:scenario_contexts] = [] if description[:scenario_contexts].nil?
39
+ @description[:stories] = [] if description[:stories].nil?
40
+ @description[:id] = SecureRandom.hex(4) if description[:id].nil?
41
+ @description[:rootpath] = File.dirname(caller[0]) if description[:rootpath].nil?
42
+
43
+ @rootpath = @description[:rootpath]
44
+ @id = @description[:id]
45
+ @scenario_contexts = @description[:scenario_contexts]
46
+ @stories = @description[:stories]
47
+
48
+ if @scenario_contexts.empty? && @stories.empty? && !@description[:status].nil?
49
+ @status = @description[:status]
50
+ else
51
+ @status = Marso.item_with_stronger_status(@stories, @scenario_contexts).status
52
+ end
53
+
54
+ @tree_position = 0
55
+ @header = get_header(@id, @status, @description)
56
+ @color_theme = get_color_theme(@status)
57
+ @text = get_text(@header, @description)
58
+ end
59
+
60
+ # Returns the combination of the feature's scenario contexts and the
61
+ # scenario contexts under each feature's stories
62
+ def all_scenario_contexts
63
+ @scenario_contexts | self.stories_scenario_contexts
64
+ end
65
+
66
+ # Returns all the scenario contexts under each feature's stories
67
+ def stories_scenario_contexts
68
+ @stories.map { |s| s.scenario_contexts }.flatten
69
+ end
70
+
71
+ private
72
+
73
+ def validate_arguments(description, ctx)
74
+ raise ArgumentError, "Argument 'description' cannot be nil" if description.nil?
75
+ raise ArgumentError, "Argument 'description' must be a Hash" unless description.is_a?(Hash)
76
+ raise ArgumentError, "Argument 'ctx' must be a Hash" unless ctx.is_a?(Hash)
77
+ unless description[:scenario_contexts].nil?
78
+ unless description[:scenario_contexts].empty?
79
+ raise ArgumentError, "Argument 'description[:scenario_contexts]' must be an Array" unless description[:scenario_contexts].is_a?(Array)
80
+ offender = description[:scenario_contexts].detect { |x| !x.is_a?(Marso::ScenarioContext) }.class
81
+ raise ArgumentError, "One value inside 'description[:scenario_contexts]' is of type #{offender}. The only type allowed is Marso::ScenarioContext" unless offender == NilClass
82
+ end
83
+ end
84
+ end
85
+
86
+ def get_header(id, status, description)
87
+ header = "Feature #{id}: #{description[:name]}"
88
+
89
+ case status
90
+ when :passed
91
+ return "#{header}: PASSED"
92
+ when :none
93
+ return header
94
+ when :failed_no_component
95
+ return "#{header}: FAILED - No scenarios or stories found"
96
+ else
97
+ return "#{header}: FAILED"
98
+ end
99
+ end
100
+
101
+ def get_color_theme(status)
102
+ case status
103
+ when :passed
104
+ :green
105
+ when :failed_no_component
106
+ :red
107
+ when :failed
108
+ :red
109
+ when :error
110
+ :red
111
+ else
112
+ :light_yellow
113
+ end
114
+ end
115
+
116
+ def get_text(header, description)
117
+ feat_parts = [header]
118
+ feat_parts << "In order to #{description[:in_order_to]}" if description.key?(:in_order_to)
119
+ feat_parts << "As a #{description[:as_a]}" if description.key?(:as_a)
120
+ feat_parts << "I #{description[:i]}" if description.key?(:i)
121
+ feat_parts.join("\n")
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,60 @@
1
+ require_relative '../../helpers/componenthelper'
2
+
3
+ module Marso
4
+ module FeatureLoad
5
+
6
+ # Load feature's components based on the following mode:
7
+ # => :none
8
+ # => :stories
9
+ # => :stories_with_scenarios
10
+ # => :scenario_contexts
11
+ # => :all
12
+ def load(mode)
13
+ case mode
14
+ when :none
15
+ return self
16
+ when :stories
17
+ return load_stories
18
+ when :stories_with_scenarios
19
+ return load_stories :with_scenarios
20
+ when :scenario_contexts
21
+ return load_scenario_contexts
22
+ when :all
23
+ return self.load(:stories_with_scenarios).load(:scenario_contexts)
24
+ else
25
+ raise ArgumentError, "Mode #{mode} is not supported. Use one of the following: :stories, :scenario_contexts, :all"
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def load_scenario_contexts
32
+ new_ctx = @ctx.clone
33
+ new_ctx[:feature_id] = @id
34
+ file_path_pattern = File.join(@rootpath, 'scenarios/*.rb')
35
+
36
+ scenario_ctxs = Marso.load_components(:scenario_context, file_path_pattern, new_ctx)
37
+
38
+ new_description = @description.clone
39
+ new_description[:scenario_contexts] = scenario_ctxs
40
+ return Feature.new(new_description, new_ctx)
41
+ end
42
+
43
+ # include_mode (optional):
44
+ # => :none - (Default) Only display the story's description
45
+ # => :with_scenarios - Display the story's description as well as all its
46
+ # scenarios' description
47
+ def load_stories(include_mode=:none)
48
+ new_ctx = @ctx.clone
49
+ new_ctx[:feature_id] = @id
50
+ file_path_pattern = File.join(@rootpath, 'stories/*/*.rb')
51
+
52
+ stories = Marso.load_components(:story, file_path_pattern, new_ctx)
53
+ .map { |s| include_mode == :with_scenarios ? s.load(:scenario_contexts) : s}
54
+
55
+ new_description = @description.clone
56
+ new_description[:stories] = stories
57
+ return Feature.new(new_description, new_ctx)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,73 @@
1
+ require 'colorize'
2
+ require_relative '../../helpers/texthelper'
3
+
4
+ module Marso
5
+ module FeaturePublish
6
+ include TextHelper
7
+
8
+ def colorized_text
9
+ self.text.colorize(self.color_theme)
10
+ end
11
+
12
+ # include_mode => Symbol that defines what should be included in the
13
+ # feature's description. Possible values are:
14
+ # :none - (Default) Only display the feature's description
15
+ # :with_stories - Display the feature description as well as all its
16
+ # stories' description
17
+ # :with_stories_scenarios - Display the feature description as well
18
+ # as all its stories' description
19
+ # (including their scenarios)
20
+ # :with_scenarios - Display the feature description as well as all its
21
+ # scenarios' description
22
+ # :with_all - Display the feature description as well as both all its
23
+ # stories(including their scenarios) and scenarios descriptions
24
+ def indented_colorized_details(include_mode=:none)
25
+
26
+ get_scenario_ctxs_text_a = lambda { |f|
27
+ f.scenario_contexts.map { |scn| scn.indented_colorized_text }
28
+ }
29
+
30
+ get_stories_text_a = lambda { |f|
31
+ f.stories.map { |s| s.indented_colorized_text }
32
+ }
33
+
34
+ get_stories_scenarios_text_a = lambda { |f|
35
+ f.stories.map { |s|
36
+ [s.indented_colorized_text]
37
+ .concat(s.scenario_contexts # add scenarios' text under each story
38
+ .map { |scn| scn.indented_colorized_text })
39
+ .join("\n") }
40
+ }
41
+
42
+ get_indented_colored_text = lambda { |f|
43
+ case include_mode
44
+ when :none
45
+ f.indented_colorized_text
46
+ when :with_scenarios
47
+ [f.indented_colorized_text]
48
+ .concat(get_scenario_ctxs_text_a.call(f)) # add scenarios' text under each feat
49
+ .join("\n")
50
+ when :with_stories
51
+ [f.indented_colorized_text]
52
+ .concat(get_stories_text_a.call(f)) # add stories' text under each feat
53
+ .join("\n")
54
+ when :with_stories_scenarios
55
+ [f.indented_colorized_text]
56
+ .concat(get_stories_scenarios_text_a.call(f)) # add stories' text under each feat
57
+ .join("\n")
58
+ when :with_all
59
+ [f.indented_colorized_text]
60
+ .concat(get_scenario_ctxs_text_a.call(f)) # add scenarios' text under each feat
61
+ .concat(get_stories_scenarios_text_a.call(f)) # add stories' text under each feat
62
+ .join("\n")
63
+ else
64
+ raise ArgumentError, ":#{include_mode} is not a valid argument. " +
65
+ "Please choose one of the following:\n" +
66
+ "- #{[:none, :with_scenarios, :with_stories, :with_stories_scenarios, :with_all].join('\n- ')}"
67
+ end
68
+ }
69
+
70
+ return get_indented_colored_text.call(self)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,279 @@
1
+ require 'colorize'
2
+ require 'securerandom'
3
+ require_relative '../../config'
4
+ require_relative 'scenario_publish'
5
+
6
+ module Marso
7
+
8
+ class ScenarioContext
9
+ attr_reader :id, :before_run, :get_scenario, :after_run, :ctx, :status,
10
+ :description, :story_id, :feature_id
11
+
12
+ def initialize(description, ctx)
13
+ validate_arguments(description, ctx)
14
+
15
+ @description = description.clone
16
+
17
+ @description[:id] = SecureRandom.hex(4) if description[:id].nil?
18
+ @description[:status] = :none if description[:status].nil?
19
+ @ctx=ctx.clone
20
+
21
+ @id = @description[:id]
22
+ @before_run=@description[:before_run]
23
+ @get_scenario=@description[:get_scenario]
24
+ @after_run=@description[:after_run]
25
+ @status=@description[:status]
26
+
27
+ @story_id = ctx[:story_id]
28
+ @feature_id = ctx[:feature_id]
29
+ end
30
+
31
+ def run
32
+ @before_run.call(@description[:id], @ctx) unless @before_run.nil?
33
+ s = @get_scenario.call(@id, @ctx)
34
+ runned_scenario = s.run
35
+ @after_run.call(@id, @ctx) unless @after_run.nil?
36
+
37
+ updated_description = @description.clone
38
+ updated_description[:get_scenario] = proc { runned_scenario }
39
+ updated_description[:status] = runned_scenario.status
40
+
41
+ return ScenarioContext.new(updated_description, @ctx)
42
+ end
43
+
44
+ def puts_scenario
45
+ s = @get_scenario.call(@id, @ctx)
46
+ s.puts_description
47
+ end
48
+
49
+ def indented_colorized_text
50
+ s = @get_scenario.call(@id, @ctx)
51
+ s.indented_colorized_text
52
+ end
53
+
54
+ private
55
+ def validate_arguments(description, ctx)
56
+ raise ArgumentError, "Argument 'ctx' must be a Hash" unless ctx.is_a?(Hash)
57
+ raise ArgumentError, "Argument 'description' cannot be nil" if description.nil?
58
+ raise ArgumentError, "Argument 'description' must define a :get_scenario key that points to a closure that returns a Marso::Scenario object" if description[:get_scenario].nil?
59
+ end
60
+ end
61
+
62
+ class Scenario
63
+ include ScenarioPublish
64
+
65
+ ISSUES_LIST = [:error, :failed, :cancelled]
66
+
67
+ @@color_options=nil
68
+ @@color_options_size=0
69
+
70
+ attr_reader :name, :steps, :id, :status, :color_theme,
71
+ :cancel_steps_upon_issues, :realtime_step_stdout, :ctx, :story_id,
72
+ :feature_id, :header, :tree_position
73
+
74
+ # description: Hash defined as follow
75
+ # :id => Arbitrary number or string. Default is randomly generated
76
+ # (hex string (length 8))
77
+ # :name => Scenario's name
78
+ # :steps => array of Step objects
79
+ # :color_theme => color from gem 'colorize'(e.g. :blue). That allows
80
+ # to visually group all steps from the same scenario.
81
+ # Default is randomly choosen from the available set
82
+ # :cancel_steps_upon_issues => Boolean. If true, all steps defined after
83
+ # a broken step(i.e. step in status :failed,
84
+ # :error, or :cancelled) will not be
85
+ # executed, and will all be set so their
86
+ # status is :cancelled.
87
+ #
88
+ # If defined, it overrides the
89
+ # Config.cancel_steps_upon_issues setting.
90
+ # :realtime_step_stdout => Boolean. If true, the result of each step is
91
+ # output to the console in realtime rather than
92
+ # waiting for the entire scenario to finish and
93
+ # then display all the steps all at once. Setting
94
+ # that config to true may make reading the output
95
+ # harder when multiple scenarios are executed in
96
+ # paralell.Steps of different scenarios may
97
+ # indeed be intertwined in the console.
98
+ #
99
+ # If defined, it overrides the
100
+ # Config.realtime_step_stdout setting.
101
+ def initialize(description, ctx={})
102
+ validate_arguments(description, ctx)
103
+
104
+ if @@color_options.nil?
105
+ @@color_options = String.colors
106
+ @@color_options_size = @@color_options.size
107
+ end
108
+
109
+ @name = description[:name]
110
+ @ctx = ctx.clone
111
+
112
+ @tree_position = 0
113
+ @tree_position+=1 unless ctx[:story_id].nil?
114
+ @tree_position+=1 unless ctx[:feature_id].nil?
115
+
116
+ @story_id = ctx[:story_id]
117
+ @feature_id = ctx[:feature_id]
118
+
119
+ @id =
120
+ description.key?(:id) ?
121
+ description[:id] :
122
+ SecureRandom.hex(4)
123
+
124
+ @status =
125
+ description.key?(:status) ?
126
+ description[:status] :
127
+ :none
128
+
129
+ @color_theme =
130
+ description.key?(:color_theme) ?
131
+ description[:color_theme] :
132
+ @@color_options[rand(@@color_options_size)]
133
+
134
+ @steps =
135
+ description.key?(:steps) ?
136
+ description[:steps].map { |s| Step.new(s.text, @id, @color_theme, s.status, &s.block) } :
137
+ []
138
+
139
+ @header = get_header(@id, @ctx)
140
+
141
+ @cancel_steps_upon_issues =
142
+ description.key?(:cancel_steps_upon_issues) ?
143
+ description[:cancel_steps_upon_issues] :
144
+ Marso::Config.get(:cancel_steps_upon_issues)
145
+
146
+ @realtime_step_stdout =
147
+ description.key?(:realtime_step_stdout) ?
148
+ description[:realtime_step_stdout] :
149
+ Marso::Config.get(:realtime_step_stdout)
150
+ end
151
+
152
+ def given(assumption_text, *args, &block)
153
+ return add_step(:given, assumption_text, *args, &block)
154
+ end
155
+
156
+ def and(assumption_text, *args, &block)
157
+ return add_step(:and, assumption_text, *args, &block)
158
+ end
159
+
160
+ def when(assumption_text, *args, &block)
161
+ return add_step(:when, assumption_text, *args, &block)
162
+ end
163
+
164
+ def then(assumption_text, *args, &block)
165
+ return add_step(:then, assumption_text, *args, &block)
166
+ end
167
+
168
+ def but(assumption_text, *args, &block)
169
+ return add_step(:but, assumption_text, *args, &block)
170
+ end
171
+
172
+ # include_id will prepend the scenario id to the step's description.
173
+ # This can be useful in the case where each step is being output to the
174
+ # console in realtime. In that situation multiple steps from multiple
175
+ # scenarios may be intertwined if they are executed concurently. Without
176
+ # the scenario id, it may be difficult to identify which step belongs to
177
+ # which scenario
178
+ def text(include_id=false)
179
+ return
180
+ "{@header}: #{name}\n" +
181
+ (@steps.any? ? @steps.map { |s| s.text(include_id) }.join("\n") : "")
182
+ end
183
+
184
+ def run
185
+ previous_step_status = nil
186
+ scenario_status = :passed
187
+ no_issues = true
188
+
189
+ processed_steps = @steps.map { |s|
190
+ runned_step = run_step(s, previous_step_status)
191
+
192
+ print_indented(runned_step.print_description) if @realtime_step_stdout
193
+
194
+ previous_step_status = runned_step.status
195
+
196
+ if no_issues
197
+ case previous_step_status
198
+ when :error
199
+ no_issues = false
200
+ scenario_status = :error
201
+ when :failed
202
+ no_issues = false
203
+ scenario_status = :failed
204
+ when :cancelled
205
+ no_issues = false
206
+ scenario_status = :failed
207
+ end
208
+ end
209
+
210
+ runned_step
211
+ }
212
+
213
+ updated_scenario = Scenario.new(
214
+ {
215
+ :id => @id,
216
+ :name => @name,
217
+ :steps => processed_steps,
218
+ :status => scenario_status,
219
+ :color_theme => @color_theme
220
+ },
221
+ @ctx)
222
+
223
+ return updated_scenario
224
+ end
225
+
226
+ private
227
+
228
+ def validate_arguments(description, ctx)
229
+ raise ArgumentError, "Argument 'description' must be a Hash" unless description.is_a?(Hash)
230
+ raise ArgumentError, "Argument 'ctx' must be a Hash" unless ctx.is_a?(Hash)
231
+ end
232
+
233
+ def add_step(step_type, assumption_text, *args, &block)
234
+ body_msg = nil
235
+ status = :none
236
+ step_name = step_type.to_s.capitalize
237
+
238
+ begin
239
+ body_msg = "#{step_name} " + assumption_text % args
240
+ rescue Exception => e
241
+ status = :error
242
+ body_msg =
243
+ "#{assumption_text}: ERROR\n" +
244
+ "args: #{args.nil? ? '' : args.join(',')}\n" +
245
+ "#{e.message}\n" +
246
+ "#{e.backtrace}"
247
+ end
248
+
249
+ new_step_series = @steps | [Step.new(body_msg, @id, color_theme, status, &block)]
250
+
251
+ return Scenario.new(
252
+ {
253
+ :id => @id,
254
+ :name => @name,
255
+ :steps => new_step_series,
256
+ :status => @status,
257
+ :color_theme => @color_theme
258
+ },
259
+ @ctx)
260
+ end
261
+
262
+ def run_step(step, previous_step_status)
263
+ if ISSUES_LIST.include?(previous_step_status) && @cancel_steps_upon_issues
264
+ cancelled_step = Step.new(step.text, @id, @color_theme, :cancelled, &step.block)
265
+ return cancelled_step.execute
266
+ else
267
+ return step.run
268
+ end
269
+ end
270
+
271
+ def get_header(id, ctx)
272
+ header = []
273
+ header << "Feature #{ctx[:feature_id]}" unless ctx[:feature_id].nil?
274
+ header << "Story #{ctx[:story_id]}" unless ctx[:story_id].nil?
275
+ header << "Scenario #{id}"
276
+ header.join(" - ")
277
+ end
278
+ end
279
+ end