Module: Marso

Defined in:
lib/marso/config.rb,
lib/marso/version.rb,
lib/marso/launcher.rb,
lib/marso/factories.rb,
lib/marso/messages/errors.rb,
lib/marso/domain/step/step.rb,
lib/marso/domain/story/story.rb,
lib/marso/helpers/texthelper.rb,
lib/marso/helpers/statushelper.rb,
lib/marso/toolbelt/fiberpiping.rb,
lib/marso/domain/feature/feature.rb,
lib/marso/domain/story/story_load.rb,
lib/marso/helpers/componenthelper.rb,
lib/marso/domain/scenario/scenario.rb,
lib/marso/domain/step/step_publish.rb,
lib/marso/domain/story/story_publish.rb,
lib/marso/domain/feature/feature_load.rb,
lib/marso/domain/feature/feature_publish.rb,
lib/marso/domain/scenario/scenario_publish.rb

Defined Under Namespace

Modules: FeatureLoad, FeaturePublish, Messages, ScenarioPublish, StepPublish, StoryLoad, StoryPublish, TextHelper Classes: Config, Enumerate, Feature, FiberFilter, FiberProjection, FiberProjectionMany, Scenario, ScenarioContext, Step, Story

Constant Summary collapse

VERSION =
"0.1.29059"

Class Method Summary collapse

Class Method Details

.components(component_type, file_path_pattern, ctx = {}) ⇒ Object

component_type:

:feature
:story
:scenario_context


35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/marso/helpers/componenthelper.rb', line 35

def self.components(component_type, file_path_pattern, ctx={})
  Enumerate.from(Dir[file_path_pattern])
    .where { |file|
      load file
      Object.const_defined?("MarsoContext") && Object.const_get("MarsoContext").respond_to?(component_type)
    }
    .select { |file|
      component = Object.const_get("MarsoContext").send(component_type, ctx)
      class_name = component_type.to_s.split("_").map { |x| x.capitalize }.join
      raise ArgumentError, "Method MarsoContext.#{component_type} cannot return nil" if component.nil?
      raise ArgumentError, "Method MarsoContext.#{component_type} must return an object of class Marso::Story" unless component.class.to_s == "Marso::#{class_name}"
      component
    }
end

.core_components_query(component_type, options = {}) ⇒ Object

options: Hash defined as follow

:rootpath => Path of the folder containing the 'stories' folder. If not
             specified, then the default is to set it to the current
             caller's location
:select => Single story's id or Array of story's ids. The execution
           of stories will be restricted to those ids only
:include_mode => Symbol that defines what should be included in the
                 feature's description. Possible values are:
     => :none - (Default) Only display the feature's description
     => :with_stories - Display the feature description as well as all its
                        stories' description
     => :with_stories_scenarios - Display the feature description as well
                                  as all its stories' description
                                  (including their scenarios)
     => :with_scenarios - Display the feature description as well as all its
                          scenarios' description
     => :with_all - Display the feature description as well as both all its
                    stories(including their scenarios) and scenarios descriptions


297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/marso/launcher.rb', line 297

def self.core_components_query(component_type, options={})
  file_pattern = nil

  case component_type
  when :feature
    file_pattern = 'features/*/*.rb'
  when :story
    file_pattern = 'stories/*/*.rb'
  when :scenario_context
    file_pattern = 'scenarios/*.rb'
  else
    raise ArgumentError, ":#{component_type} is not a valid component_type. " +
    "Valid types are #{[:feature, :story, :scenario_context].join(', ')}"
  end

  file_path_pattern = File.join(options.rootpath, file_pattern)
  load_mode = options.include_mode.to_load_mode

  components = Marso.components(component_type, file_path_pattern)
  ids_selection = options.ids_selection

  query =  ids_selection.any? ? components.where { |x| ids_selection.include?(x.id) } : components

  if component_type == :scenario_context
    return query
  else
    return query.select { |c| c.load(load_mode) }
  end
end

.item_with_stronger_status(x, y) ⇒ Object



172
173
174
# File 'lib/marso/helpers/statushelper.rb', line 172

def self.item_with_stronger_status(x, y)
  return x.status>=y.status ? x : y
end

.load_components(component_type, file_path_pattern, ctx = {}) ⇒ Object

component_type:

:feature
:story
:scenario_context


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File 'lib/marso/helpers/componenthelper.rb', line 9

def self.load_components(component_type, file_path_pattern, ctx={})
  components = []

  Dir[file_path_pattern].each { |file|

    load file

    file_contains_marso_component = Object.const_defined?("MarsoContext") && Object.const_get("MarsoContext").respond_to?(component_type)

    if file_contains_marso_component
      component = Object.const_get("MarsoContext").send(component_type, ctx)
      class_name = component_type.to_s.split("_").map { |x| x.capitalize }.join
      raise ArgumentError, "Method MarsoContext.#{component_type} cannot return nil" if component.nil?
      raise ArgumentError, "Method MarsoContext.#{component_type} must return an object of class Marso::Story" unless component.class.to_s == "Marso::#{class_name}"

      components << component
    end
  }

  return components
end

.openNewBrowser(browser = nil) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# File 'lib/marso/factories.rb', line 7

def openNewBrowser(browser=nil)
  browser = !browser.nil? && browser.class == Symbol ? browser : :firefox
  profile = nil
  case browser
  when :firefox
    profile = Selenium::WebDriver::Firefox::Profile.new
  else
    raise "Marso does not support '#{browser}' yet. The only supported browser is currently firefox"
  end

  profile['browser.cache.disk.enable'] = false

  b = Watir::Browser.new browser, :profile => profile
  return b
end

.reorganize_scenariocontexts_into_components(scenario_contexts, origin_stories, origin_features) ⇒ Object

This method reorganized a collection of scenario contexts so they are regrouped under their parent components(origin_stories or origin_features). The stories that results from that reorganization will also be regrouped under their respective features Arguments:

- scenario_contexts: Array of scenarios contexts that need to be
                     reorganized
- origin_stories: Array of stories OR single story, that are a known
                  parent of some of the scenario_contexts
- origin_features: Array of features OR single feature, that are a known
                   parent of some of the scenario_contexts


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/marso/helpers/componenthelper.rb', line 62

def self.reorganize_scenariocontexts_into_components(scenario_contexts, origin_stories, origin_features)
  original_features = origin_features.is_a?(Array) ? origin_features : [origin_features]
  original_stories = origin_stories.is_a?(Array) ? origin_stories : [origin_stories]

  updated_stories = []
  scenario_contexts_per_feature_ids = []
  features_updated_with_stories = []
  updated_features = []

  scenario_contexts
    .group_by { |x| x.story_id }
    .each { |k,v|
      if k != nil # scenarios which belong to stories
        story = original_stories.detect { |x| x.id == k }
        updated_story_description = story.description.clone
        updated_story_description[:scenario_contexts] = v
        updated_story_ctx = story.ctx.clone
        updated_story = Story.new(updated_story_description, updated_story_ctx)
        updated_stories << updated_story
      else # scenarios which do not belong to any stories, and therefore probably belong straight to a feature
        scenario_contexts_per_feature_ids = v.group_by { |y| y.feature_id }
      end
    }

  # Update original stories that seem to not have any scenario_contexts associated with them
  original_stories.each { |s|
    unless updated_stories.any? { |x| x.id == s.id }
      new_description = s.description.clone
      new_description[:status] = :failed_no_scenarios
      updated_stories << Marso::Story.new(new_description, s.ctx)
    end
  }

  # Regroup updated stories under their respective features
  updated_stories
    .group_by { |x| x.feature_id }
    .each { |k,v|
      if k != nil # stories which belong to features
        feat = original_features.detect { |x| x.id == k }
        updated_feat_description = feat.description.clone
        updated_feat_description[:stories] = v
        updated_feat_ctx = feat.ctx.clone
        updated_feat = Feature.new(updated_feat_description, updated_feat_ctx)
        features_updated_with_stories << updated_feat
      else
        # not sure what to do yet. Normally, scenario_contexts with no feature
        # or story associated with them should not happen
      end
    }

  # Add all the original features that do not contain any updated stories
  # to the list of updated features. This is needed so that the updated features
  # list contain all the orignial features so that we can use it for the next step
  original_features.each { |x|
    (features_updated_with_stories << x) unless features_updated_with_stories.any? { |y| y.id == x.id }
  }

  # Regroup scenario contexts that are not associated with any stories under
  # their respective feature
  updated_features = features_updated_with_stories.map { |x|
    if scenario_contexts_per_feature_ids.key?(x.id) # found scenarios for this feature
      new_description = x.description.clone
      new_description[:scenario_contexts] = scenario_contexts_per_feature_ids[x.id]
      new_ctx = x.ctx.clone
      Feature.new(new_description, new_ctx)
    else # no scenarios for this feature. Check if it has stories, otherwise flag it as failed
      if x.stories.any?
        x
      else
       new_description = x.description.clone
       new_description[:status] = :failed_no_component
       new_ctx = x.ctx.clone
       Feature.new(new_description, new_ctx)
     end
    end
  }

  return [updated_features, updated_stories]
end

.run_features(options = {}) ⇒ Object

options: Hash defined as follow

:rootpath => Path of the folder containing the 'stories' folder. If not
             specified, then the default is to set it to the current
             caller's location
:select => Single story's id or Array of story's ids. The execution
           of stories will be restricted to those ids only
:include_mode => Symbol that defines what should be included in the
                 feature's description. Possible values are:
     => :none - (Default) Only display the feature's description
     => :with_stories - Display the feature description as well as all its
                        stories' description
     => :with_stories_scenarios - Display the feature description as well
                                  as all its stories' description
                                  (including their scenarios)
     => :with_scenarios - Display the feature description as well as all its
                          scenarios' description
     => :with_all - Display the feature description as well as both all its
                    stories(including their scenarios) and scenarios descriptions


45
46
47
48
49
50
51
52
# File 'lib/marso/launcher.rb', line 45

def self.run_features(options={})
  options[:include_mode] = :with_all

  Marso.core_components_query(:feature, options)
    .select_many { |f| f.all_scenario_contexts }
    .select { |scn| scn.run }
    .execute
end

.run_features_async(options = {}) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/marso/launcher.rb', line 54

def self.run_features_async(options={})
  options[:include_mode] = :with_all

  all_features = Marso.core_components_query(:feature, options).to_a
  _all_feat = Enumerate.from(all_features)
  all_stories = _all_feat.select_many { |f| f.stories }.to_a
  all_scenario_ctxs = _all_feat.select_many { |f| f.all_scenario_contexts }.to_a

  scenarios_per_features = Hash[all_features
    .reject { |f| f.scenario_contexts.empty? }
    .group_by { |f| f.id }
    .map { |f_id, f|
      [
        f_id, # Hash key
        {     # Hash value
          :scenarios => f[0].scenario_contexts.map { |scn| {:processed => false, :id => scn.id, :original_scn => scn}},
          :original_feature => f[0],
          :processed => false
        }
      ]}]

  scenarios_per_stories = Hash[all_stories
    .reject { |s| s.scenario_contexts.empty? }
    .group_by { |s| s.id }
    .map { |s_id, s|
      [
        s_id, # Hash key
        {     # Hash value
          :scenarios => s[0].scenario_contexts.map { |scn| {:processed => false, :id => scn.id, :original_scn => scn}},
          :original_story => s[0],
          :processed => false
        }
      ]}]

  stories_per_features = Hash[all_features
    .reject { |f| f.stories.empty? }
    .group_by { |f| f.id }
    .map { |f_id, f|
      [
        f_id, # Hash key
        {     # Hash value
          :stories => f[0].stories.map { |s| {:processed => false, :id => s.id }},
          :processed => false
        }
      ]}]

  features_with_status = Hash[all_features.map { |f|
    [
      f.id, # Hash key
      {     # Hash value
        :original_feature => f,
        :processed => false
      }
    ]}]

  task_size = all_scenario_ctxs.size
  updated_scenario_ctxs = []

  EM.run do
    all_scenario_ctxs.each { |scn|
      EM.defer(
        proc { scn.run },
        lambda { |updated_scenario_ctx|
          updated_scenario_ctxs << updated_scenario_ctx
          task_size -= 1
          EM.stop if task_size < 1

          puts updated_scenario_ctx.indented_colorized_text
          scn_id = updated_scenario_ctx.id
          story_id = updated_scenario_ctx.story_id
          feat_id = updated_scenario_ctx.feature_id
          scenarios_per_story = scenarios_per_stories[story_id]
          scenarios_per_feat = scenarios_per_features[feat_id]
          stories_per_feat = stories_per_features[feat_id]
          feature_with_status = features_with_status[feat_id]
          story_completed = false
          feature_completed = false
          unless scenarios_per_story.nil?
            item = scenarios_per_story[:scenarios].detect { |scn| scn[:id] == scn_id }
            unless item.nil?
              item[:processed] = true
              item[:updated_scn] = updated_scenario_ctx
              story_completed = scenarios_per_story[:scenarios].all? { |scn| scn[:processed] }
              if story_completed
                # 1. Update the story
                scenarios_per_story[:processed] = true
                original_story = scenarios_per_story[:original_story]
                udt_story_desc = original_story.description.clone
                udt_story_ctx = original_story.ctx.clone
                udt_story_desc[:scenario_contexts] = scenarios_per_story[:scenarios].map { |scn| scn[:updated_scn]}
                updated_story = Marso::Story.new(udt_story_desc, udt_story_ctx)
                puts updated_story.indented_colorized_text
                scenarios_per_story[:updated_story] = updated_story
                unless stories_per_feat.nil? # 2. Cascade the new story status upon the feature
                  # 2.1. Update the status of the story under the feature
                  story_item = stories_per_feat[:stories].detect { |s| s[:id] == story_id }
                  unless story_item.nil?
                    story_item[:processed] = true
                    # 2.2. Check all stories under that feature are noew completed
                    all_stories_for_that_feature_completed = stories_per_feat[:stories].all? { |s| s[:processed] }
                    if all_stories_for_that_feature_completed
                      stories_per_feat[:processed] = true
                      # 2.3. Check if that feature is now fully completed
                      feature_completed = scenarios_per_feat.nil? || scenarios_per_feat[:processed]
                      if feature_completed
                        feature_with_status[:processed] = true
                        original_feature = feature_with_status[:original_feature]
                        udt_feat_desc = original_feature.description.clone
                        udt_feat_ctx = original_feature.ctx.clone
                        # 2.3.1. Update feature with all updated stories
                        unless stories_per_feat.nil?
                          feat_story_ids = stories_per_feat[:stories].map { |s| s[:id]}
                          upt_stories = scenarios_per_stories
                            .select { |s_id| feat_story_ids.include?(s_id) }
                            .values.map { |x| x[:updated_story] }
                          udt_feat_desc[:stories] = upt_stories
                        end
                        # 2.3.2. Update feature with all updated scenarios
                        unless scenarios_per_feat.nil?
                          udt_feat_desc[:scenario_contexts] = scenarios_per_feat[:scenarios].map { |scn| scn[:updated_scn]}
                        end
                        updated_feature = Marso::Feature.new(udt_feat_desc, udt_feat_ctx)
                        puts updated_feature.indented_colorized_text
                        feature_with_status[:updated_feature] = updated_feature
                      end
                    end
                  end
                end
              end
            end
          end

          unless scenarios_per_feat.nil?
            item = scenarios_per_feat[:scenarios].detect { |scn| scn[:id] == scn_id }
            unless item.nil?
              item[:processed] = true
              item[:updated_scn] = updated_scenario_ctx
              all_scenarios_for_that_feature_completed = scenarios_per_feat[:scenarios].all? { |scn| scn[:processed] }
              if all_scenarios_for_that_feature_completed
                # 1. Update the feature
                scenarios_per_feat[:processed] = true
                # 2. Check if that feature is now fully completed
                feature_completed = stories_per_feat.nil? || stories_per_feat[:processed]
                if feature_completed
                  feature_with_status[:processed] = true
                  original_feature = feature_with_status[:original_feature]
                  udt_feat_desc = original_feature.description.clone
                  udt_feat_ctx = original_feature.ctx.clone
                  # 2.2.1. Update feature with all updated stories
                  unless stories_per_feat.nil?
                    feat_story_ids = stories_per_feat[:stories].map { |s| s[:id]}
                    upt_stories = scenarios_per_stories
                      .select { |s_id| feat_story_ids.include?(s_id) }
                      .values.map { |x| x[:updated_story] }
                    udt_feat_desc[:stories] = upt_stories
                  end
                  # 2.2.2. Update feature with all updated scenarios
                  unless scenarios_per_feat.nil?
                    udt_feat_desc[:scenario_contexts] = scenarios_per_feat[:scenarios].map { |scn| scn[:updated_scn]}
                  end
                  updated_feature = Marso::Feature.new(udt_feat_desc, udt_feat_ctx)
                  puts updated_feature.indented_colorized_text
                  feature_with_status[:updated_feature] = updated_feature
                end
              end
            end
          end
      })
    }
  end
end

.show_features_text(options = {}) ⇒ Object

options: Hash defined as follow

:rootpath => Path of the folder containing the 'stories' folder. If not
             specified, then the default is to set it to the current
             caller's location
:select => Single story's id or Array of story's ids. The execution
           of stories will be restricted to those ids only
:include_mode => Symbol that defines what should be included in the
                 feature's description. Possible values are:
     => :none - (Default) Only display the feature's description
     => :with_stories - Display the feature description as well as all its
                        stories' description
     => :with_stories_scenarios - Display the feature description as well
                                  as all its stories' description
                                  (including their scenarios)
     => :with_scenarios - Display the feature description as well as all its
                          scenarios' description
     => :with_all - Display the feature description as well as both all its
                    stories(including their scenarios) and scenarios descriptions


244
245
246
247
248
# File 'lib/marso/launcher.rb', line 244

def self.show_features_text(options={})
  Marso.core_components_query(:feature, options)
    .select { |f| puts f.indented_colorized_details(options.include_mode) }
    .execute
end

.show_scenarios_text(options = {}) ⇒ Object

options: Hash defined as follow

:rootpath => Path of the folder containing the 'stories' folder. If not
             specified, then the default is to set it to the current
             caller's location
:select => Single story's id or Array of story's ids. The execution
           of stories will be restricted to those ids only


273
274
275
276
277
# File 'lib/marso/launcher.rb', line 273

def self.show_scenarios_text(options={})
  Marso.core_components_query(:scenario_context, options)
    .select { |s| puts s.indented_colorized_text }
    .execute
end

.show_stories_text(options = {}) ⇒ Object

options: Hash defined as follow

:rootpath => Path of the folder containing the 'stories' folder. If not
             specified, then the default is to set it to the current
             caller's location
:select => Single story's id or Array of story's ids. The execution
           of stories will be restricted to those ids only
:include_mode => Symbol that defines what should be included in the
                 feature's description. Possible values are:
     => :none - (Default) Only display the feature's description
     => :with_scenarios - Display the feature description as well as all its
                          scenarios' description


261
262
263
264
265
# File 'lib/marso/launcher.rb', line 261

def self.show_stories_text(options={})
  Marso.core_components_query(:story, options)
    .select { |s| puts s.indented_colorized_details(options.include_mode) }
    .execute
end