features 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of features might be problematic. Click here for more details.
- data/Manifest +21 -0
- data/README.markdown +0 -0
- data/Rakefile +14 -0
- data/features.gemspec +30 -0
- data/features.rb +10 -0
- data/lib/ObjC/objc_feature.rb +64 -0
- data/lib/ObjC/objc_scenario.rb +89 -0
- data/lib/ObjC/objc_step.rb +70 -0
- data/lib/ObjC/objc_suite.rb +152 -0
- data/lib/Rails/rails_feature.rb +62 -0
- data/lib/Rails/rails_scenario.rb +85 -0
- data/lib/Rails/rails_step.rb +60 -0
- data/lib/Rails/rails_suite.rb +161 -0
- data/lib/feature.rb +2 -0
- data/lib/objc.rb +6 -0
- data/lib/parser.rb +37 -0
- data/lib/rails.rb +6 -0
- data/lib/scenario.rb +2 -0
- data/lib/step.rb +2 -0
- data/lib/string_extension.rb +9 -0
- data/lib/suite.rb +2 -0
- metadata +96 -0
data/Manifest
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Manifest
|
2
|
+
README.markdown
|
3
|
+
Rakefile
|
4
|
+
features.gemspec
|
5
|
+
features.rb
|
6
|
+
lib/ObjC/objc_feature.rb
|
7
|
+
lib/ObjC/objc_scenario.rb
|
8
|
+
lib/ObjC/objc_step.rb
|
9
|
+
lib/ObjC/objc_suite.rb
|
10
|
+
lib/Rails/rails_feature.rb
|
11
|
+
lib/Rails/rails_scenario.rb
|
12
|
+
lib/Rails/rails_step.rb
|
13
|
+
lib/Rails/rails_suite.rb
|
14
|
+
lib/feature.rb
|
15
|
+
lib/objc.rb
|
16
|
+
lib/parser.rb
|
17
|
+
lib/rails.rb
|
18
|
+
lib/scenario.rb
|
19
|
+
lib/step.rb
|
20
|
+
lib/string_extension.rb
|
21
|
+
lib/suite.rb
|
data/README.markdown
ADDED
File without changes
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'echoe'
|
4
|
+
|
5
|
+
Echoe.new('features', '0.1.0') do |p|
|
6
|
+
p.description = "Plaintext User Stories Parser supporting native programming languages."
|
7
|
+
p.url = "http://features.rubyforge.org"
|
8
|
+
p.author = "Matthias Hennemeyer"
|
9
|
+
p.email = "mhennemeyer@me.com"
|
10
|
+
p.ignore_pattern = ["spec/**/*"]
|
11
|
+
p.development_dependencies = []
|
12
|
+
end
|
13
|
+
|
14
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
data/features.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{features}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Matthias Hennemeyer"]
|
9
|
+
s.date = %q{2009-08-30}
|
10
|
+
s.description = %q{Plaintext User Stories Parser supporting native programming languages.}
|
11
|
+
s.email = %q{mhennemeyer@me.com}
|
12
|
+
s.extra_rdoc_files = ["README.markdown", "lib/ObjC/objc_feature.rb", "lib/ObjC/objc_scenario.rb", "lib/ObjC/objc_step.rb", "lib/ObjC/objc_suite.rb", "lib/Rails/rails_feature.rb", "lib/Rails/rails_scenario.rb", "lib/Rails/rails_step.rb", "lib/Rails/rails_suite.rb", "lib/feature.rb", "lib/objc.rb", "lib/parser.rb", "lib/rails.rb", "lib/scenario.rb", "lib/step.rb", "lib/string_extension.rb", "lib/suite.rb"]
|
13
|
+
s.files = ["Manifest", "README.markdown", "Rakefile", "features.gemspec", "features.rb", "lib/ObjC/objc_feature.rb", "lib/ObjC/objc_scenario.rb", "lib/ObjC/objc_step.rb", "lib/ObjC/objc_suite.rb", "lib/Rails/rails_feature.rb", "lib/Rails/rails_scenario.rb", "lib/Rails/rails_step.rb", "lib/Rails/rails_suite.rb", "lib/feature.rb", "lib/objc.rb", "lib/parser.rb", "lib/rails.rb", "lib/scenario.rb", "lib/step.rb", "lib/string_extension.rb", "lib/suite.rb"]
|
14
|
+
s.homepage = %q{http://features.rubyforge.org}
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Features", "--main", "README.markdown"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{features}
|
18
|
+
s.rubygems_version = %q{1.3.5}
|
19
|
+
s.summary = %q{Plaintext User Stories Parser supporting native programming languages.}
|
20
|
+
|
21
|
+
if s.respond_to? :specification_version then
|
22
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
23
|
+
s.specification_version = 3
|
24
|
+
|
25
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
26
|
+
else
|
27
|
+
end
|
28
|
+
else
|
29
|
+
end
|
30
|
+
end
|
data/features.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
lib = File.dirname(__FILE__) + "/lib"
|
2
|
+
|
3
|
+
require lib + "/string_extension.rb"
|
4
|
+
require lib + "/feature.rb"
|
5
|
+
require lib + "/scenario.rb"
|
6
|
+
require lib + "/step.rb"
|
7
|
+
require lib + "/suite.rb"
|
8
|
+
require lib + "/parser.rb"
|
9
|
+
require lib + "/rails.rb"
|
10
|
+
require lib + "/objc.rb"
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class ObjcFeature < Feature
|
2
|
+
attr_reader :scenarios, :scenario_keyword,
|
3
|
+
:title, :body, :parent, :parser,
|
4
|
+
:given_scenario_keyword, :keyword
|
5
|
+
|
6
|
+
def initialize(hash={})
|
7
|
+
@title = hash[:title]
|
8
|
+
@body = hash[:body]
|
9
|
+
@parent = hash[:parent]
|
10
|
+
@keyword = hash[:keyword] || "Feature:"
|
11
|
+
@scenario_keyword = hash[:scenario_keyword] || "Scenario:"
|
12
|
+
@given_scenario_keyword = hash[:given_scenario_keyword] || "GivenScenario:"
|
13
|
+
|
14
|
+
raise "No title given" unless title
|
15
|
+
raise "No body given" unless body
|
16
|
+
end
|
17
|
+
|
18
|
+
def story
|
19
|
+
body.split(/#{scenario_keyword}/)[0].split(/#{keyword}\s#{title}/).join(" ").strip
|
20
|
+
end
|
21
|
+
|
22
|
+
def story_html
|
23
|
+
story.split("\n").join(" <br />")
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_scenarios
|
27
|
+
title_body_arr = Parser.title_and_body_by_keyword_from_string({
|
28
|
+
:string => body,
|
29
|
+
:keyword => scenario_keyword
|
30
|
+
})
|
31
|
+
@scenarios = title_body_arr.map {|hash| ObjcScenario.new(hash.update({
|
32
|
+
:parent => self,
|
33
|
+
:given_scenario_keyword => given_scenario_keyword
|
34
|
+
}))}
|
35
|
+
@scenarios.each {|scenario| scenario.collect_steps}
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
<<-END
|
41
|
+
@interface #{test_case_name} : OMFeature
|
42
|
+
@end
|
43
|
+
@implementation #{test_case_name}
|
44
|
+
#{scenarios.map {|s| s.to_s}.join(" ")}
|
45
|
+
@end
|
46
|
+
END
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_html
|
50
|
+
<<-END
|
51
|
+
<div class="feature">
|
52
|
+
<h2 class="feature_title">#{keyword} #{title}</h2>
|
53
|
+
<p class="story">
|
54
|
+
#{story_html}
|
55
|
+
</p>
|
56
|
+
#{scenarios.map {|s| s.to_html }.join(" \n")}
|
57
|
+
</div>
|
58
|
+
END
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_case_name
|
62
|
+
"#{title.remove_invalid_chars.split(/\s+/).map {|w| w.capitalize}.join('')}Test"
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class ObjcScenario < Scenario
|
2
|
+
attr_reader :steps, :lines, :given_scenario_keyword,
|
3
|
+
:title, :body, :parent, :passed
|
4
|
+
|
5
|
+
def initialize(hash)
|
6
|
+
@title = hash[:title]
|
7
|
+
@body = hash[:body]
|
8
|
+
@parent = hash[:parent]
|
9
|
+
@given_scenario_keyword = hash[:given_scenario_keyword] || "GivenScenario:"
|
10
|
+
|
11
|
+
raise "No title given" unless title
|
12
|
+
raise "No body given" unless body
|
13
|
+
end
|
14
|
+
|
15
|
+
def keyword
|
16
|
+
parent.scenario_keyword
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_html
|
20
|
+
<<-END
|
21
|
+
<div class="scenario #{passed? ? "passed" : "failed"}">
|
22
|
+
<h2 class="scenario_title">#{keyword} #{title}</h2>
|
23
|
+
#{steps.map {|s| s.to_html}.join(" \n")}
|
24
|
+
</div>
|
25
|
+
END
|
26
|
+
end
|
27
|
+
|
28
|
+
def passed?
|
29
|
+
!!@passed
|
30
|
+
end
|
31
|
+
|
32
|
+
def verify_status(results="")
|
33
|
+
test_case_name = parent.test_case_name
|
34
|
+
#Test Case '-[SayHelloTest testWithABlankObject]' failed (0.001 seconds).
|
35
|
+
results =~ /Test\sCase\s'-\[#{test_case_name}\s#{test_name}\]'\s(\w+)/
|
36
|
+
match = $1
|
37
|
+
if match =~ /failed/
|
38
|
+
@passed = false
|
39
|
+
elsif match =~ /passed/
|
40
|
+
@passed = true
|
41
|
+
else
|
42
|
+
raise "Can't read results File"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def collect_steps
|
47
|
+
|
48
|
+
if has_given_scenarios?
|
49
|
+
@body = expand_given_scenarios_in_body.strip
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
@lines = body.split(/\n+/).map {|s| s.strip}
|
54
|
+
raise "No Steps found" if lines.empty?
|
55
|
+
|
56
|
+
@steps = parse_lines
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def expand_given_scenarios_in_body
|
61
|
+
raise "No associated Feature" unless parent
|
62
|
+
raise "Associated Feature has no Scenarios" unless parent.scenarios
|
63
|
+
body.gsub(/#{given_scenario_keyword}(.*)/) do |m|
|
64
|
+
existing_scenario = parent.scenarios.detect {|s| s.title == $1.strip }
|
65
|
+
existing_scenario ? existing_scenario.body.strip : ""
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_lines
|
70
|
+
lines.map {|l| ObjcStep.new({:title => l, :body => l}).aggregate!}
|
71
|
+
end
|
72
|
+
|
73
|
+
def has_given_scenarios?
|
74
|
+
!!(body =~ /#{given_scenario_keyword}(.*)/)
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
<<-END
|
79
|
+
-(void) #{test_name}
|
80
|
+
{
|
81
|
+
#{steps.map {|s| s.to_s}.join(" ")}
|
82
|
+
}
|
83
|
+
END
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_name
|
87
|
+
"test#{title.remove_invalid_chars.split(/\s+/).map {|w| w.capitalize}.join('')}"
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class ObjcStep < Step
|
2
|
+
attr_reader :message
|
3
|
+
|
4
|
+
attr_reader :title, :body, :parent
|
5
|
+
def initialize(hash)
|
6
|
+
@title = hash[:title]
|
7
|
+
@body = hash[:body]
|
8
|
+
@parent = hash[:parent]
|
9
|
+
raise "No title given" unless title
|
10
|
+
raise "No body given" unless body
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_html
|
14
|
+
s = <<-END
|
15
|
+
<h3 class="step">#{title}</h3>
|
16
|
+
END
|
17
|
+
s.strip
|
18
|
+
end
|
19
|
+
|
20
|
+
def aggregate!
|
21
|
+
@message = first_part + args_string
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def first_part
|
26
|
+
body.gsub(/\s+/,"_").gsub(/'[^']*'/, "__").remove_invalid_chars
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_args?
|
30
|
+
!args.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def args
|
34
|
+
@args ||= body.scan(/'([^']*)'/).map {|a| a[0]}
|
35
|
+
end
|
36
|
+
|
37
|
+
def args_string
|
38
|
+
if has_args?
|
39
|
+
([":@\"#{args[0]}\""] +
|
40
|
+
(args[1..args.length] || []).map { |a| "arg:@\"#{a}\"" }).join(" ")
|
41
|
+
else
|
42
|
+
""
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_s
|
47
|
+
"[self #{message}];"
|
48
|
+
end
|
49
|
+
|
50
|
+
def parameter_string
|
51
|
+
if has_args?
|
52
|
+
s = ":(NSString *)arg "
|
53
|
+
(args[1..args.length] || []).each_with_index do |a, i|
|
54
|
+
s << "arg:(NSString *)arg#{i+2} "
|
55
|
+
end
|
56
|
+
s
|
57
|
+
else
|
58
|
+
""
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_ocmethod
|
63
|
+
<<-END
|
64
|
+
-(void) #{first_part + parameter_string}
|
65
|
+
{
|
66
|
+
|
67
|
+
}
|
68
|
+
END
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
class ObjcSuite < Suite
|
2
|
+
attr_reader :feature_files, :feature_files_path,
|
3
|
+
:feature_file_suffix, :feature_files_as_strings,
|
4
|
+
:features, :test_cases_file,
|
5
|
+
:feature_class_header_files, :given_scenario_keyword,
|
6
|
+
:feature_keyword, :scenario_keyword,
|
7
|
+
:project_name, :passed
|
8
|
+
|
9
|
+
def initialize(hash)
|
10
|
+
@feature_files_path = hash[:feature_files_path]
|
11
|
+
@feature_file_suffix = hash[:feature_file_suffix] || "feature"
|
12
|
+
@test_cases_file = hash[:test_cases_file]
|
13
|
+
@feature_class_header_files = hash[:feature_class_header_files] || ["OMFeature.h"]
|
14
|
+
@feature_keyword = hash[:feature_keyword] || "Feature:"
|
15
|
+
@scenario_keyword = hash[:scenario_keyword] || "Scenario:"
|
16
|
+
@given_scenario_keyword = hash[:given_scenario_keyword] || "GivenScenario:"
|
17
|
+
@project_name = hash[:project_name] || "Features"
|
18
|
+
@feature_files = all_feature_files
|
19
|
+
@feature_files_as_strings = all_feature_files_as_strings
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_output_file_and_open_in_browser(file)
|
23
|
+
results = ""
|
24
|
+
File.open(file) do |f|
|
25
|
+
f.readlines.each do |l|
|
26
|
+
results << l
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
html = parse_results(results).html
|
31
|
+
|
32
|
+
%x(touch '/tmp/out.html' && echo '#{html}' > /tmp/out.html && open '/tmp/out.html' )
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def parse_results(results="")
|
38
|
+
parse
|
39
|
+
@passed = true
|
40
|
+
features.each do |f|
|
41
|
+
f.scenarios.each do |s|
|
42
|
+
s.verify_status(results)
|
43
|
+
@passed &&= s.passed?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def passed?
|
50
|
+
@passed
|
51
|
+
end
|
52
|
+
|
53
|
+
def html
|
54
|
+
<<-END
|
55
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
56
|
+
<html>
|
57
|
+
<head>
|
58
|
+
<style type="text/css">
|
59
|
+
body {
|
60
|
+
font-family: Lucida Grande;
|
61
|
+
background-color: #FEFEFE;
|
62
|
+
padding: 5px;
|
63
|
+
}
|
64
|
+
* {-webkit-border-radius: 10px;}
|
65
|
+
body {background-color: #EEE; -webkit-border-radius: 0px;}
|
66
|
+
.feature { background-color: #CCC;padding: 20px; margin: 20px;}
|
67
|
+
.scenario { background-color: #EEE; padding: 20px; margin: 20px}
|
68
|
+
.failed {border: 1px solid #C88; background-color: #B77; }
|
69
|
+
.passed {border: 1px solid #8C8; background-color: #7B7; }
|
70
|
+
.feature_title {font-size: 24pt; }
|
71
|
+
.story { font-size: 18pt; padding-left: 40px;}
|
72
|
+
#indicator { width: 100%; height: 20px; }
|
73
|
+
#wrap { background-color: #EEE; padding: 20px }
|
74
|
+
</style></head>
|
75
|
+
</head>
|
76
|
+
<body>
|
77
|
+
<div id="wrap">
|
78
|
+
<div id="header">
|
79
|
+
<div id="indicator" class="#{passed? ? "passed" : "failed"}"> </div>
|
80
|
+
<h1 class="project_name">#{project_name}</h1>
|
81
|
+
|
82
|
+
</div>
|
83
|
+
#{features.map {|f| f.to_html }.join(" \n")}
|
84
|
+
</div>
|
85
|
+
</body>
|
86
|
+
</html>
|
87
|
+
END
|
88
|
+
end
|
89
|
+
|
90
|
+
def valid?
|
91
|
+
unique_feature_test_case_names?
|
92
|
+
end
|
93
|
+
|
94
|
+
def unique_feature_test_case_names?
|
95
|
+
feature_test_case_name = features.map {|f| f.test_case_name }
|
96
|
+
feature_test_case_name == feature_test_case_name.uniq
|
97
|
+
end
|
98
|
+
|
99
|
+
def run
|
100
|
+
parse
|
101
|
+
raise "Invalid: Duplicated Titles" unless valid?
|
102
|
+
File.open(test_cases_file, "w") { |f| f.puts self }
|
103
|
+
end
|
104
|
+
|
105
|
+
def parse
|
106
|
+
parse_features
|
107
|
+
parse_feature_scenarios
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_s
|
111
|
+
<<-END
|
112
|
+
#{feature_class_header_files.map { |f| "#import \"" + f + "\"" }.join(" ")}
|
113
|
+
#{features.map {|f| f.to_s }.join(" ")}
|
114
|
+
END
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_features
|
118
|
+
title_body_arr = Parser.title_and_body_by_keyword_from_string({
|
119
|
+
:string => feature_files_as_strings.join(" "),
|
120
|
+
:keyword => feature_keyword
|
121
|
+
})
|
122
|
+
@features = title_body_arr.map {|hash| ObjcFeature.new(hash.update({:keyword => feature_keyword}))}
|
123
|
+
end
|
124
|
+
|
125
|
+
def parse_feature_scenarios
|
126
|
+
features.each do |feature|
|
127
|
+
feature.parse_scenarios
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def all_feature_files
|
132
|
+
all_entries_in_feature_files_path = Dir.new(feature_files_path).entries
|
133
|
+
feature_entries = all_entries_in_feature_files_path.select do |file|
|
134
|
+
!!(file =~ /\.#{feature_file_suffix}$/)
|
135
|
+
end
|
136
|
+
feature_entries.map do |file|
|
137
|
+
feature_files_path + "/" + file
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def all_feature_files_as_strings
|
142
|
+
feature_files.map do |file|
|
143
|
+
feature_string = ""
|
144
|
+
File.open(file) do |f|
|
145
|
+
f.readlines.each do |l|
|
146
|
+
feature_string << l
|
147
|
+
end
|
148
|
+
end
|
149
|
+
feature_string
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class RailsFeature < Feature
|
2
|
+
attr_reader :scenarios, :scenario_keyword,
|
3
|
+
:title, :body, :parent, :parser,
|
4
|
+
:given_scenario_keyword, :keyword
|
5
|
+
|
6
|
+
def initialize(hash={})
|
7
|
+
@title = hash[:title]
|
8
|
+
@body = hash[:body]
|
9
|
+
@parent = hash[:parent]
|
10
|
+
@keyword = hash[:keyword] || "Feature:"
|
11
|
+
@scenario_keyword = hash[:scenario_keyword] || "Scenario:"
|
12
|
+
@given_scenario_keyword = hash[:given_scenario_keyword] || "GivenScenario:"
|
13
|
+
|
14
|
+
raise "No title given" unless title
|
15
|
+
raise "No body given" unless body
|
16
|
+
end
|
17
|
+
|
18
|
+
def story
|
19
|
+
body.split(/#{scenario_keyword}/)[0].split(/#{keyword}\s#{title}/).join(" ").strip
|
20
|
+
end
|
21
|
+
|
22
|
+
def story_html
|
23
|
+
story.split("\n").join(" <br />")
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_scenarios
|
27
|
+
title_body_arr = Parser.title_and_body_by_keyword_from_string({
|
28
|
+
:string => body,
|
29
|
+
:keyword => scenario_keyword
|
30
|
+
})
|
31
|
+
@scenarios = title_body_arr.map {|hash| RailsScenario.new(hash.update({
|
32
|
+
:parent => self,
|
33
|
+
:given_scenario_keyword => given_scenario_keyword
|
34
|
+
}))}
|
35
|
+
@scenarios.each {|scenario| scenario.collect_steps}
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
<<-END
|
41
|
+
class #{test_case_name} < FeaturesTestCaseClass
|
42
|
+
#{scenarios.map {|s| s.to_s}.join(" ")}
|
43
|
+
end
|
44
|
+
END
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_html
|
48
|
+
<<-END
|
49
|
+
<div class="feature">
|
50
|
+
<h2 class="feature_title">#{keyword} #{title}</h2>
|
51
|
+
<p class="story">
|
52
|
+
#{story_html}
|
53
|
+
</p>
|
54
|
+
#{scenarios.map {|s| s.to_html }.join(" \n")}
|
55
|
+
</div>
|
56
|
+
END
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_case_name
|
60
|
+
"#{title.remove_invalid_chars.split(/\s+/).map {|w| w.capitalize}.join('')}Test"
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class RailsScenario < Scenario
|
2
|
+
attr_reader :steps, :lines, :given_scenario_keyword,
|
3
|
+
:title, :body, :parent, :passed
|
4
|
+
|
5
|
+
def initialize(hash)
|
6
|
+
@title = hash[:title]
|
7
|
+
@body = hash[:body]
|
8
|
+
@parent = hash[:parent]
|
9
|
+
@given_scenario_keyword = hash[:given_scenario_keyword] || "GivenScenario:"
|
10
|
+
|
11
|
+
raise "No title given" unless title
|
12
|
+
raise "No body given" unless body
|
13
|
+
end
|
14
|
+
|
15
|
+
def keyword
|
16
|
+
parent.scenario_keyword
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_html
|
20
|
+
<<-END
|
21
|
+
<div class="scenario #{passed? ? "passed" : "failed"}">
|
22
|
+
<h2 class="scenario_title">#{keyword} #{title}</h2>
|
23
|
+
#{steps.map {|s| s.to_html}.join(" \n")}
|
24
|
+
</div>
|
25
|
+
END
|
26
|
+
end
|
27
|
+
|
28
|
+
def passed?
|
29
|
+
!!@passed
|
30
|
+
end
|
31
|
+
|
32
|
+
def verify_status(results="")
|
33
|
+
test_case_name = parent.test_case_name
|
34
|
+
|
35
|
+
# 2) Failure:
|
36
|
+
# test_AnotherFailingOne(FeatureTest) [/test/integration/feature_test.rb:16]:
|
37
|
+
# <false> is not true.
|
38
|
+
|
39
|
+
result = (results =~ /^\s*#{test_name}\(#{test_case_name}\)/)
|
40
|
+
@passed = result ? false : true
|
41
|
+
end
|
42
|
+
|
43
|
+
def collect_steps
|
44
|
+
|
45
|
+
if has_given_scenarios?
|
46
|
+
@body = expand_given_scenarios_in_body.strip
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
@lines = body.split(/\n+/).map {|s| s.strip}
|
51
|
+
raise "No Steps found" if lines.empty?
|
52
|
+
|
53
|
+
@steps = parse_lines
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def expand_given_scenarios_in_body
|
58
|
+
raise "No associated Feature" unless parent
|
59
|
+
raise "Associated Feature has no Scenarios" unless parent.scenarios
|
60
|
+
body.gsub(/#{given_scenario_keyword}(.*)/) do |m|
|
61
|
+
existing_scenario = parent.scenarios.detect {|s| s.title == $1.strip }
|
62
|
+
existing_scenario ? existing_scenario.body.strip : ""
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_lines
|
67
|
+
lines.map {|l| RailsStep.new({:body => l}).aggregate!}
|
68
|
+
end
|
69
|
+
|
70
|
+
def has_given_scenarios?
|
71
|
+
!!(body =~ /#{given_scenario_keyword}(.*)/)
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
<<-END
|
76
|
+
def #{test_name}
|
77
|
+
#{steps.map {|s| s.to_s}.join("; ")}
|
78
|
+
end
|
79
|
+
END
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_name
|
83
|
+
"test_#{title.remove_invalid_chars.split(/\s+/).map {|w| w.capitalize}.join('')}"
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
class RailsStep < Step
|
2
|
+
attr_reader :message
|
3
|
+
|
4
|
+
attr_reader :body, :parent
|
5
|
+
def initialize(hash)
|
6
|
+
@body = hash[:body]
|
7
|
+
@parent = hash[:parent]
|
8
|
+
raise "No body given" unless body
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_html
|
12
|
+
s = <<-END
|
13
|
+
<h3 class="step">#{body}</h3>
|
14
|
+
END
|
15
|
+
s.strip
|
16
|
+
end
|
17
|
+
|
18
|
+
def aggregate!
|
19
|
+
@message = first_part + args_string
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def first_part
|
24
|
+
body.gsub(/\s+/,"_").gsub(/'[^']*'/, "__").remove_invalid_chars.sub(/./) do |first_char|
|
25
|
+
first_char.downcase
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_args?
|
30
|
+
!args.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def args
|
34
|
+
@args ||= body.scan(/'([^']*)'/).map {|a| a[0]}
|
35
|
+
end
|
36
|
+
|
37
|
+
def args_string
|
38
|
+
if has_args?
|
39
|
+
"(" + args.map {|a| '"' + a.to_s + '"'}.join(", ") + ")"
|
40
|
+
else
|
41
|
+
""
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_s
|
46
|
+
message
|
47
|
+
end
|
48
|
+
|
49
|
+
def parameter_string
|
50
|
+
if has_args?
|
51
|
+
s = "(arg"
|
52
|
+
(args[1..args.length] || []).each_with_index do |a, i|
|
53
|
+
s << ", arg#{i+2}"
|
54
|
+
end
|
55
|
+
s + ")"
|
56
|
+
else
|
57
|
+
""
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
class RailsSuite < Suite
|
2
|
+
attr_reader :feature_files, :feature_files_path,
|
3
|
+
:feature_file_suffix, :feature_files_as_strings,
|
4
|
+
:features, :test_cases_file,
|
5
|
+
:feature_keyword, :scenario_keyword,
|
6
|
+
:project_name, :passed,
|
7
|
+
:given_scenario_keyword, :features_helper
|
8
|
+
|
9
|
+
def initialize(hash)
|
10
|
+
@feature_files_path = hash[:feature_files_path]
|
11
|
+
@feature_file_suffix = hash[:feature_file_suffix] || "feature"
|
12
|
+
@test_cases_file = hash[:test_cases_file]
|
13
|
+
@features_helper = File.expand_path(hash[:features_helper])
|
14
|
+
@feature_keyword = hash[:feature_keyword] || "Feature:"
|
15
|
+
@scenario_keyword = hash[:scenario_keyword] || "Scenario:"
|
16
|
+
@given_scenario_keyword = hash[:given_scenario_keyword] || "GivenScenario:"
|
17
|
+
@project_name = hash[:project_name] || "Features"
|
18
|
+
@feature_files = all_feature_files
|
19
|
+
@feature_files_as_strings = all_feature_files_as_strings
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_output_file_and_open_in_browser(file)
|
23
|
+
results = ""
|
24
|
+
File.open(file) do |f|
|
25
|
+
f.readlines.each do |l|
|
26
|
+
results << l
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
html = parse_results(results).html
|
31
|
+
|
32
|
+
%x(touch '/tmp/out.html' && echo '#{html}' > /tmp/out.html && open '/tmp/out.html' )
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_results_and_open_in_safari(results)
|
37
|
+
html = parse_results(results).html
|
38
|
+
open_in_safari(html)
|
39
|
+
end
|
40
|
+
|
41
|
+
def open_in_safari(html)
|
42
|
+
%x(touch '/tmp/out.html' && echo '#{html}' > /tmp/out.html && open '/tmp/out.html' )
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def parse_results(results="")
|
47
|
+
parse
|
48
|
+
@passed = true
|
49
|
+
features.each do |f|
|
50
|
+
f.scenarios.each do |s|
|
51
|
+
s.verify_status(results)
|
52
|
+
@passed &&= s.passed?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def passed?
|
59
|
+
@passed
|
60
|
+
end
|
61
|
+
|
62
|
+
def html
|
63
|
+
<<-END
|
64
|
+
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
65
|
+
<html>
|
66
|
+
<head>
|
67
|
+
<style type="text/css">
|
68
|
+
body {
|
69
|
+
font-family: Lucida Grande;
|
70
|
+
background-color: #FEFEFE;
|
71
|
+
padding: 5px;
|
72
|
+
}
|
73
|
+
* {-webkit-border-radius: 10px;}
|
74
|
+
body {background-color: #EEE; -webkit-border-radius: 0px;}
|
75
|
+
.feature { background-color: #E0E0E0;padding: 20px; margin: 20px;}
|
76
|
+
.scenario { background-color: #EEE; padding: 20px; margin: 20px}
|
77
|
+
.failed {border: 1px solid #C88; background-color: #B77; }
|
78
|
+
.passed {border: 1px solid #8C8; background-color: #7B7; }
|
79
|
+
.feature_title {font-size: 24pt; }
|
80
|
+
.story { font-size: 18pt; padding-left: 40px;}
|
81
|
+
#indicator { width: 100%; height: 20px; }
|
82
|
+
#wrap { background-color: #EEE; padding: 20px }
|
83
|
+
</style></head>
|
84
|
+
</head>
|
85
|
+
<body>
|
86
|
+
<div id="wrap">
|
87
|
+
<div id="header">
|
88
|
+
<div id="indicator" class="#{passed? ? "passed" : "failed"}"> </div>
|
89
|
+
<h1 class="project_name">#{project_name}</h1>
|
90
|
+
|
91
|
+
</div>
|
92
|
+
#{features.map {|f| f.to_html }.join(" \n")}
|
93
|
+
</div>
|
94
|
+
</body>
|
95
|
+
</html>
|
96
|
+
END
|
97
|
+
end
|
98
|
+
|
99
|
+
def valid?
|
100
|
+
unique_feature_test_case_names?
|
101
|
+
end
|
102
|
+
|
103
|
+
def unique_feature_test_case_names?
|
104
|
+
feature_test_case_name = features.map {|f| f.test_case_name }
|
105
|
+
feature_test_case_name == feature_test_case_name.uniq
|
106
|
+
end
|
107
|
+
|
108
|
+
def run
|
109
|
+
parse
|
110
|
+
raise "Invalid: Duplicated Titles" unless valid?
|
111
|
+
File.open(test_cases_file, "w") { |f| f.puts self }
|
112
|
+
end
|
113
|
+
|
114
|
+
def parse
|
115
|
+
parse_features
|
116
|
+
parse_feature_scenarios
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_s
|
120
|
+
<<-END
|
121
|
+
require "#{features_helper}"
|
122
|
+
#{features.map {|f| f.to_s }.join(" ")}
|
123
|
+
END
|
124
|
+
end
|
125
|
+
|
126
|
+
def parse_features
|
127
|
+
title_body_arr = Parser.title_and_body_by_keyword_from_string({
|
128
|
+
:string => feature_files_as_strings.join(" "),
|
129
|
+
:keyword => feature_keyword
|
130
|
+
})
|
131
|
+
@features = title_body_arr.map {|hash| RailsFeature.new(hash.update({:keyword => feature_keyword}))}
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_feature_scenarios
|
135
|
+
features.each do |feature|
|
136
|
+
feature.parse_scenarios
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def all_feature_files
|
141
|
+
all_entries_in_feature_files_path = Dir.new(feature_files_path).entries
|
142
|
+
feature_entries = all_entries_in_feature_files_path.select do |file|
|
143
|
+
!!(file =~ /\.#{feature_file_suffix}$/)
|
144
|
+
end
|
145
|
+
feature_entries.map do |file|
|
146
|
+
feature_files_path + "/" + file
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def all_feature_files_as_strings
|
151
|
+
feature_files.map do |file|
|
152
|
+
feature_string = ""
|
153
|
+
File.open(file) do |f|
|
154
|
+
f.readlines.each do |l|
|
155
|
+
feature_string << l
|
156
|
+
end
|
157
|
+
end
|
158
|
+
feature_string
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/lib/feature.rb
ADDED
data/lib/objc.rb
ADDED
data/lib/parser.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
class Parser
|
2
|
+
attr_reader :string, :keyword, :create, :parsing
|
3
|
+
def initialize(hash)
|
4
|
+
@string = hash[:string]
|
5
|
+
@keyword = hash[:keyword]
|
6
|
+
raise "No Keyword given" unless keyword
|
7
|
+
raise "Nothing to parse given" unless string
|
8
|
+
raise "No #{keyword} given." unless /#{keyword}(.*)/.match(string)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.title_and_body_by_keyword_from_string(hash)
|
12
|
+
parser = self.new(hash)
|
13
|
+
titles = parser.parse_titles
|
14
|
+
bodies = parser.parse_bodies
|
15
|
+
arr = []
|
16
|
+
titles.each_with_index do |t,i|
|
17
|
+
arr << {:title => t, :body => bodies[i]}
|
18
|
+
end
|
19
|
+
arr
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_titles
|
23
|
+
string.scan(/(^|\s+)#{keyword}(.*)/).map {|a| a[1].strip}
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_bodies
|
27
|
+
bodies = string.split(/(^|\s+)(#{keyword}.*)/).reject {|s| s.empty? }.reject {|s| s.only_whitespace? }.map {|s| s.strip}
|
28
|
+
start = 0
|
29
|
+
bodies.each_with_index do |o,i|
|
30
|
+
if (o =~ /^\s*#{keyword}.*\s*$/)
|
31
|
+
start = i
|
32
|
+
break
|
33
|
+
end
|
34
|
+
end
|
35
|
+
bodies[start..bodies.length].select {|s| !(s =~ /^\s*#{keyword}.*\s*$/) }
|
36
|
+
end
|
37
|
+
end
|
data/lib/rails.rb
ADDED
data/lib/scenario.rb
ADDED
data/lib/step.rb
ADDED
data/lib/suite.rb
ADDED
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: features
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthias Hennemeyer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-08-30 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Plaintext User Stories Parser supporting native programming languages.
|
17
|
+
email: mhennemeyer@me.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
- lib/ObjC/objc_feature.rb
|
25
|
+
- lib/ObjC/objc_scenario.rb
|
26
|
+
- lib/ObjC/objc_step.rb
|
27
|
+
- lib/ObjC/objc_suite.rb
|
28
|
+
- lib/Rails/rails_feature.rb
|
29
|
+
- lib/Rails/rails_scenario.rb
|
30
|
+
- lib/Rails/rails_step.rb
|
31
|
+
- lib/Rails/rails_suite.rb
|
32
|
+
- lib/feature.rb
|
33
|
+
- lib/objc.rb
|
34
|
+
- lib/parser.rb
|
35
|
+
- lib/rails.rb
|
36
|
+
- lib/scenario.rb
|
37
|
+
- lib/step.rb
|
38
|
+
- lib/string_extension.rb
|
39
|
+
- lib/suite.rb
|
40
|
+
files:
|
41
|
+
- Manifest
|
42
|
+
- README.markdown
|
43
|
+
- Rakefile
|
44
|
+
- features.gemspec
|
45
|
+
- features.rb
|
46
|
+
- lib/ObjC/objc_feature.rb
|
47
|
+
- lib/ObjC/objc_scenario.rb
|
48
|
+
- lib/ObjC/objc_step.rb
|
49
|
+
- lib/ObjC/objc_suite.rb
|
50
|
+
- lib/Rails/rails_feature.rb
|
51
|
+
- lib/Rails/rails_scenario.rb
|
52
|
+
- lib/Rails/rails_step.rb
|
53
|
+
- lib/Rails/rails_suite.rb
|
54
|
+
- lib/feature.rb
|
55
|
+
- lib/objc.rb
|
56
|
+
- lib/parser.rb
|
57
|
+
- lib/rails.rb
|
58
|
+
- lib/scenario.rb
|
59
|
+
- lib/step.rb
|
60
|
+
- lib/string_extension.rb
|
61
|
+
- lib/suite.rb
|
62
|
+
has_rdoc: true
|
63
|
+
homepage: http://features.rubyforge.org
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --line-numbers
|
69
|
+
- --inline-source
|
70
|
+
- --title
|
71
|
+
- Features
|
72
|
+
- --main
|
73
|
+
- README.markdown
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
version:
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: "1.2"
|
87
|
+
version:
|
88
|
+
requirements: []
|
89
|
+
|
90
|
+
rubyforge_project: features
|
91
|
+
rubygems_version: 1.3.5
|
92
|
+
signing_key:
|
93
|
+
specification_version: 3
|
94
|
+
summary: Plaintext User Stories Parser supporting native programming languages.
|
95
|
+
test_files: []
|
96
|
+
|