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.
- data/LICENSE +20 -0
- data/README +55 -0
- data/Rakefile +101 -0
- data/bin/cukehead +7 -0
- data/lib/cukehead.rb +1 -0
- data/lib/cukehead/app.rb +241 -0
- data/lib/cukehead/feature_file_section.rb +73 -0
- data/lib/cukehead/feature_node.rb +111 -0
- data/lib/cukehead/feature_node_child.rb +58 -0
- data/lib/cukehead/feature_node_tags.rb +41 -0
- data/lib/cukehead/feature_part.rb +25 -0
- data/lib/cukehead/feature_reader.rb +78 -0
- data/lib/cukehead/feature_writer.rb +42 -0
- data/lib/cukehead/freemind_builder.rb +184 -0
- data/lib/cukehead/freemind_reader.rb +69 -0
- data/lib/cukehead/freemind_writer.rb +21 -0
- data/spec/app_spec.rb +275 -0
- data/spec/cukehead_spec.rb +104 -0
- data/spec/feature_file_section_spec.rb +93 -0
- data/spec/feature_node_spec.rb +115 -0
- data/spec/feature_part_spec.rb +37 -0
- data/spec/feature_reader_spec.rb +33 -0
- data/spec/feature_writer_spec.rb +73 -0
- data/spec/freemind_builder_spec.rb +91 -0
- data/spec/freemind_reader_spec.rb +62 -0
- data/spec/freemind_writer_spec.rb +25 -0
- data/spec/spec_helper.rb +88 -0
- metadata +93 -0
@@ -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
|