module Marso

Constants

VERSION

Public Class Methods

components(component_type, file_path_pattern, ctx={}) click to toggle source

component_type:

:feature
:story
:scenario_context
# 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={}) click to toggle source

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
# 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) click to toggle source
# 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={}) click to toggle source

component_type:

:feature
:story
:scenario_context
# 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
reorganize_scenariocontexts_into_components(scenario_contexts, origin_stories, origin_features) click to toggle source

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
# 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={}) click to toggle source

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
# 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={}) click to toggle source
# 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={}) click to toggle source

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
# 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={}) click to toggle source

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
# 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={}) click to toggle source

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

Public Instance Methods

openNewBrowser(browser=nil) click to toggle source
# 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