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 +4 -4
- data/lib/marso.rb +17 -2
- data/lib/marso/config.rb +5 -5
- data/lib/marso/domain/feature/feature.rb +124 -0
- data/lib/marso/domain/feature/feature_load.rb +60 -0
- data/lib/marso/domain/feature/feature_publish.rb +73 -0
- data/lib/marso/domain/scenario/scenario.rb +279 -0
- data/lib/marso/domain/scenario/scenario_publish.rb +17 -0
- data/lib/marso/domain/step/step.rb +95 -0
- data/lib/marso/domain/step/step_publish.rb +31 -0
- data/lib/marso/domain/story/story.rb +112 -0
- data/lib/marso/domain/story/story_load.rb +18 -0
- data/lib/marso/domain/story/story_publish.rb +41 -0
- data/lib/marso/helpers/componenthelper.rb +142 -0
- data/lib/marso/helpers/statushelper.rb +175 -0
- data/lib/marso/helpers/texthelper.rb +23 -0
- data/lib/marso/launcher.rb +326 -0
- data/lib/marso/messages/errors.rb +29 -0
- data/lib/marso/toolbelt/fiberpiping.rb +207 -0
- data/lib/marso/validation/symbol.rb +21 -0
- data/lib/marso/version.rb +1 -1
- data/marso.gemspec +1 -0
- metadata +33 -4
- data/lib/marso/assert.rb +0 -17
- data/lib/marso/scenario.rb +0 -278
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68643dc298346f9de6a9ab082f1a36186a233ee3
|
|
4
|
+
data.tar.gz: 95b856464d21e45e9c0e0739a888f6a145ab42d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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/
|
|
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
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|