marso 0.1.15089 → 0.1.29007

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.
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