cukehead 0.1.1

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.
@@ -0,0 +1,111 @@
1
+ require 'rexml/document'
2
+ require 'cukehead/feature_node_tags'
3
+ require 'cukehead/feature_node_child'
4
+
5
+ module Cukehead
6
+
7
+ # Responsible for extracting the attributes of a feature from a XML node
8
+ # (REXML::Element) and providing those attributes as text in Cucumber
9
+ # feature file format.
10
+ #
11
+ class FeatureNode
12
+
13
+ # Extracts the attributes of a feature from a mind map node.
14
+ # ===Parameters
15
+ # <tt>node</tt> - REXML::Element
16
+ #
17
+ def initialize(node)
18
+ @feature_filename = ""
19
+ @description = []
20
+ @backgrounds = []
21
+ @scenarios = []
22
+ @tags = FeatureNodeTags.new
23
+ @title = node.attributes["TEXT"]
24
+ from_mm_node node
25
+ end
26
+
27
+
28
+ # Returns the feature title as a string suitable for use as a file name.
29
+ #
30
+ def title_as_filename
31
+ result = ''
32
+ t = @title.strip.gsub(/^feature:/i, ' ').strip
33
+ t.downcase.gsub(/\ /, '_').scan(/[_0-9a-z]/) {|c| result << c }
34
+ result + '.feature'
35
+ end
36
+
37
+
38
+ # Returns a string that is either the file name given explititly in the
39
+ # mind map or a file name constructed from the feature title.
40
+ #
41
+ def filename
42
+ if @feature_filename.empty?
43
+ title_as_filename
44
+ else
45
+ @feature_filename
46
+ end
47
+ end
48
+
49
+
50
+ # Returns the file name extracted from the given text where the format
51
+ # is '<i>[file: filename.feature]</i>' or '<i>[file: "filename.feature"]</i>'.
52
+ #
53
+ def filename_from(text)
54
+ a = text.index ':'
55
+ if a.nil? then return "" end
56
+ b = text.index ']'
57
+ if b.nil? then return "" end
58
+ a += 1
59
+ if a > 5 and b > a
60
+ text.slice(a, b-a).delete('"').strip
61
+ else
62
+ ""
63
+ end
64
+ end
65
+
66
+
67
+ # Returns a string containing the attributes of this feature as text
68
+ # formatted for output to a Cucumber feature file.
69
+ #
70
+ def to_text
71
+ text = @tags.to_text('') + @title + "\n"
72
+ pad = " "
73
+ @description.each {|line| text += pad + line + "\n"}
74
+ text += "\n"
75
+ @backgrounds.each {|background| text += background.to_text(pad) + "\n"}
76
+ @scenarios.each {|scenario| text += scenario.to_text(pad) + "\n"}
77
+ text
78
+ end
79
+
80
+
81
+ private
82
+
83
+
84
+ # ===Parameters
85
+ # <tt>node</tt> - REXML::Element
86
+ #
87
+ def from_mm_node(node)
88
+ if node.has_elements?
89
+ node.elements.each do |e|
90
+ text = e.attributes["TEXT"]
91
+ if !text.nil?
92
+ case text
93
+ when /^Background:*/i
94
+ @backgrounds << FeatureNodeChild.new(e)
95
+ when /^Scenario:*/i
96
+ @scenarios << FeatureNodeChild.new(e)
97
+ when /^\[file:.*/i
98
+ @feature_filename = filename_from text
99
+ when /^Tags:*/i
100
+ @tags.from_text text
101
+ else
102
+ @description << text
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+ end
@@ -0,0 +1,58 @@
1
+ require 'rexml/document'
2
+ require 'cukehead/feature_node_tags'
3
+
4
+ module Cukehead
5
+
6
+ # Responsible for extracting the contents of a mind map node representing
7
+ # a sub-section of a feature (such as a Background or Scenario) and
8
+ # providing it as text for a feature file.
9
+ #
10
+ class FeatureNodeChild
11
+
12
+ # Extracts the feature section described in the given node.
13
+ # ===Parameters
14
+ # <tt>node</tt> - REXML::Element
15
+ #
16
+ def initialize(node)
17
+ @description = []
18
+ @tags = FeatureNodeTags.new
19
+ @title = node.attributes["TEXT"]
20
+ from_mm_node node
21
+ end
22
+
23
+
24
+ # Returns the title, tags, and descriptive text extracted from the
25
+ # mind map as a string of text formatted for output to a feature file.
26
+ # ===Parameters
27
+ # <tt>pad</tt> - String of whitespace to use to indent the section.
28
+ #
29
+ def to_text(pad)
30
+ s = "\n"
31
+ @description.each {|d| s += pad + " " + d + "\n"}
32
+ pad + @tags.to_text(pad) + @title + s
33
+ end
34
+
35
+ private
36
+
37
+ # Extracts tags and descriptive text from child nodes of the given node.
38
+ # ===Parameters
39
+ # <tt>node</tt> - REXML::Element
40
+ #
41
+ def from_mm_node(node)
42
+ if node.has_elements?
43
+ node.elements.each do |e|
44
+ text = e.attributes["TEXT"]
45
+ unless text.nil?
46
+ if text =~ /^Tags:*/i
47
+ @tags.from_text text
48
+ else
49
+ @description << text
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
@@ -0,0 +1,41 @@
1
+ module Cukehead
2
+
3
+ # Responsible for holding a string of tags extracted from a mind map
4
+ # node and providing it in the format used in a Cucumber feature file.
5
+ #
6
+ class FeatureNodeTags
7
+
8
+ def initialize
9
+ @tagstr = ''
10
+ end
11
+
12
+
13
+ # Extracts the substring containing the tags from a string in the format
14
+ # <i>Tags: tag1 tag2</i>
15
+ # ===Parameters
16
+ # <tt>text</tt> - String containing tags from mind map node text.
17
+ #
18
+ def from_text(text)
19
+ a = text.split(':')
20
+ if a.length == 2
21
+ @tagstr = a[1].strip
22
+ end
23
+ end
24
+
25
+
26
+ # Returns the string of tags extracted by from_text in the format
27
+ # needed for output to a feature file.
28
+ # ===Parameters
29
+ # <tt>pad</tt> - String of whitespace to use to indent the tags.
30
+ #
31
+ def to_text(pad)
32
+ if @tagstr.length == 0
33
+ ''
34
+ else
35
+ pad + @tagstr + "\n"
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,25 @@
1
+ module Cukehead
2
+
3
+ # Container for text from part of a feature file (feature description,
4
+ # background, scenario).
5
+ #
6
+ class FeaturePart
7
+ attr_accessor :title
8
+ attr_reader :lines
9
+ attr_accessor :tags
10
+
11
+
12
+ def initialize(title = '')
13
+ @title = title
14
+ @lines = []
15
+ @tags = []
16
+ end
17
+
18
+
19
+ def add_line(line)
20
+ @lines << line if line.strip.length > 0
21
+ end
22
+ end
23
+
24
+ end
25
+
@@ -0,0 +1,78 @@
1
+ require 'cukehead/freemind_builder'
2
+ require 'cukehead/feature_file_section'
3
+
4
+ module Cukehead
5
+
6
+ class FeatureReader
7
+
8
+ # ===Parameters
9
+ # <tt>mm_xml</tt> - Optional string containing XML representation of a
10
+ # FreeMind mind map into which a Cucumber Features node will be inserted.
11
+ #
12
+ def initialize(mm_xml = nil)
13
+ @builder = FreemindBuilder.new(mm_xml)
14
+ end
15
+
16
+
17
+ # ===Parameters
18
+ # <tt>filename</tt> - The name of the file from which text was read.
19
+ #
20
+ #<tt>text</tt> - String containing the text of a Cucumber feature file.
21
+ #
22
+ def extract_features(filename, text)
23
+ @section = nil
24
+ in_literal_text = false
25
+ literal_text = ''
26
+ tags = []
27
+ text.each do |line|
28
+ s = line.strip
29
+ #$stderr.puts "DEBUG: LINE='#{s}'"
30
+ if s == '"""'
31
+ if in_literal_text
32
+ @section.add(literal_text + "\n\"\"\"") unless @section == nil
33
+ in_literal_text = false
34
+ else
35
+ literal_text = "\"\"\"\n"
36
+ in_literal_text = true
37
+ end
38
+ else
39
+ if in_literal_text
40
+ literal_text << line
41
+ else
42
+ case s
43
+ when /^\@.*/i
44
+ tags = s.split(' ')
45
+ when /^Feature:.*/i
46
+ @section.finish unless @section == nil
47
+ @section = FeatureSection.new(@builder, line, filename)
48
+ @section.set_tags tags
49
+ tags.clear
50
+ when /^Background:.*/i
51
+ @section.finish unless @section == nil
52
+ @section = BackgroundSection.new(@builder, line)
53
+ @section.set_tags tags
54
+ tags.clear
55
+ when /^(Scenario|Scenario Outline):.*/i
56
+ @section.finish unless @section == nil
57
+ @section = ScenarioSection.new(@builder, line)
58
+ @section.set_tags tags
59
+ tags.clear
60
+ else
61
+ @section.add(line) unless @section == nil
62
+ end
63
+ end
64
+ end
65
+ end
66
+ @section.finish unless @section == nil
67
+ end
68
+
69
+
70
+ # Returns a string that is the FreeMind mind map XML.
71
+ #
72
+ def freemind_xml
73
+ @builder.xml
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,42 @@
1
+ module Cukehead
2
+
3
+ class FeatureWriter
4
+ attr_accessor :output_path
5
+ attr_accessor :overwrite
6
+ attr_reader :errors
7
+
8
+
9
+ def initialize
10
+ @output_path = File.join(Dir.getwd, 'features')
11
+ @overwrite = false
12
+ @errors = []
13
+ end
14
+
15
+
16
+ # Writes feature files to the location specified by :output_path.
17
+ # ===Parameters
18
+ # <tt>features</tt> - Hash of filename => featuretext.
19
+ #
20
+ def write_features(features)
21
+ FileUtils.mkdir_p(@output_path) unless File.directory? @output_path
22
+ ok = true
23
+ unless @overwrite
24
+ features.each {|fn, text|
25
+ filename = File.join(@output_path, fn)
26
+ if File.exists? filename
27
+ @errors << "Exists: #{filename}"
28
+ ok = false
29
+ end
30
+ }
31
+ end
32
+ if ok
33
+ features.each {|fn, text|
34
+ filename = File.join(@output_path, fn)
35
+ File.open(filename, 'w') {|f| f.write(text)}
36
+ }
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,184 @@
1
+ require 'rexml/document'
2
+
3
+ module Cukehead
4
+
5
+ class FreemindBuilder
6
+
7
+ DEFAULT_XML = "<map version='0.7.1'><node TEXT='New mind map'></node></map>"
8
+ COLOR_FEATURE = '#0000ff'
9
+ COLOR_BACKGROUND = '#ff0000'
10
+ COLOR_SCENARIO_1 = '#3d9140'
11
+ COLOR_SCENARIO_2 = '#cd8500'
12
+ COLOR_TAGS = '#B452CD'
13
+ COLOR_SYSTEM = '#a1a1a1'
14
+ FOLD_FEATURE = true
15
+ FOLD_BACKGROUND = true
16
+ FOLD_SCENARIO = true
17
+
18
+
19
+ # ===Parameters
20
+ # <tt>mm_xml</tt> - String, optional.
21
+ #
22
+ def initialize(mm_xml = nil)
23
+ if mm_xml.nil?
24
+ @mmdoc = REXML::Document.new(DEFAULT_XML)
25
+ else
26
+ @mmdoc = REXML::Document.new(mm_xml)
27
+ end
28
+ @features_path = nil
29
+ @features_node = cucumber_features_node
30
+ @scenario_count = 0
31
+ @current_feature = nil
32
+ end
33
+
34
+
35
+ # ===Parameters
36
+ # <tt>part</tt> - Cukehead::FeaturePart containing feature description.
37
+ #
38
+ # <tt>filename</tt> - String containing name of the source file for this feature.
39
+ #
40
+ def add_feature(part, filename)
41
+ add_features_path(filename) if @features_path.nil?
42
+ new_node = new_feature_node part.title
43
+ e = new_node_element feature_filename_text(filename), COLOR_SYSTEM
44
+ e.add_attribute 'LINK', filename
45
+ new_node.add_element e
46
+ unless part.tags.empty?
47
+ new_node.add_element(new_node_element('Tags: ' + part.tags.join(' '), COLOR_TAGS))
48
+ end
49
+ part.lines.each do |line|
50
+ el = new_node_element line.strip, COLOR_FEATURE
51
+ if line =~ /^\ *(As\ |In\ |I\ ).*$/
52
+ el.add_element new_bold_font_element
53
+ else
54
+ el.add_element new_italic_font_element
55
+ end
56
+ new_node.add_element el
57
+ end
58
+ @current_feature = new_node
59
+ end
60
+
61
+
62
+ # ===Parameters
63
+ # <tt>part</tt> - Cukehead::FeaturePart containing background description.
64
+ #
65
+ def add_background(part)
66
+ raise "No feature defined. Cannot add background section." if @current_feature.nil?
67
+ new_node = @current_feature.add_element(new_node_element(part.title.strip, COLOR_BACKGROUND, FOLD_BACKGROUND))
68
+ unless part.tags.empty?
69
+ new_node.add_element(new_node_element('Tags: ' + part.tags.join(' '), COLOR_TAGS))
70
+ end
71
+ part.lines.each do |line|
72
+ el = new_node_element line.strip, COLOR_BACKGROUND
73
+ if line =~ /^\ *(Given\ |When\ |Then\ |And\ |But\ ).*$/
74
+ el.add_element new_bold_font_element
75
+ else
76
+ el.add_element new_italic_font_element
77
+ end
78
+ new_node.add_element el
79
+ end
80
+ end
81
+
82
+
83
+ # ===Parameters
84
+ # <tt>part</tt> - Cukehead::FeaturePart containing scenario description.
85
+ #
86
+ def add_scenario(part)
87
+ raise "No feature defined. Cannot add scenario section." if @current_feature.nil?
88
+ @scenario_count += 1
89
+ @scenario_count.odd? ? color = COLOR_SCENARIO_1 : color = COLOR_SCENARIO_2
90
+ new_node = @current_feature.add_element(new_node_element(part.title.strip, color, FOLD_SCENARIO))
91
+ unless part.tags.empty?
92
+ new_node.add_element(new_node_element('Tags: ' + part.tags.join(' '), COLOR_TAGS))
93
+ end
94
+ part.lines.each do |line|
95
+ el = new_node_element line.strip, color
96
+ if line =~ /^\ *(Given\ |When\ |Then\ |And\ |But\ ).*$/
97
+ el.add_element new_bold_font_element
98
+ else
99
+ el.add_element new_italic_font_element
100
+ end
101
+ new_node.add_element el
102
+ end
103
+ end
104
+
105
+
106
+ def xml
107
+ @mmdoc.to_s
108
+ end
109
+
110
+ private
111
+
112
+ def new_node_element(text, color = "", folded = false)
113
+ e = REXML::Element.new "node"
114
+ e.add_attribute 'TEXT', text
115
+ e.add_attribute 'COLOR', color if color.length > 0
116
+ e.add_attribute 'FOLDED', 'true' if folded
117
+ e
118
+ end
119
+
120
+
121
+ def new_italic_font_element
122
+ e = REXML::Element.new "font"
123
+ e.add_attribute 'ITALIC', "true"
124
+ e.add_attribute 'NAME', "SansSerif"
125
+ e.add_attribute 'SIZE', "12"
126
+ e
127
+ end
128
+
129
+
130
+ def new_bold_font_element
131
+ e = REXML::Element.new "font"
132
+ e.add_attribute 'BOLD', "true"
133
+ e.add_attribute 'NAME', "SansSerif"
134
+ e.add_attribute 'SIZE', "12"
135
+ e
136
+ end
137
+
138
+
139
+ def add_cucumber_features_node
140
+ node = @mmdoc.root.elements[1]
141
+ e = new_node_element "Cucumber features:"
142
+ node.add_element e
143
+ return e
144
+ end
145
+
146
+
147
+ def cucumber_features_node
148
+ node = REXML::XPath.first(@mmdoc, '//node[attribute::TEXT="Cucumber features:"]')
149
+ if node == nil
150
+ add_cucumber_features_node
151
+ else
152
+ node
153
+ end
154
+ end
155
+
156
+
157
+ def features_path_text
158
+ '[path: ' + @features_path + ']'
159
+ end
160
+
161
+
162
+ def feature_filename_text(filename)
163
+ '[file: ' + File.basename(filename) + ']'
164
+ end
165
+
166
+
167
+ def add_features_path(filename)
168
+ @features_path = File.dirname(File.expand_path(filename))
169
+ e = new_node_element features_path_text, COLOR_SYSTEM
170
+ e.add_attribute 'LINK', @features_path
171
+ @features_node.add_element e
172
+ end
173
+
174
+
175
+ def new_feature_node(title)
176
+ e = new_node_element title.strip, COLOR_FEATURE, FOLD_FEATURE
177
+ e.add_element new_bold_font_element
178
+ @features_node.add_element e
179
+ end
180
+
181
+
182
+ end
183
+
184
+ end