cukehead 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|