crab 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ module Crab
2
+ class Update
3
+ def initialize(global_opts, cmd_opts, args)
4
+ @global_opts = global_opts
5
+ @cmd_opts = cmd_opts
6
+ @args = args
7
+
8
+ @rally = Rally.new
9
+ end
10
+
11
+ def run
12
+ opts = validate_and_fix_up_arguments
13
+
14
+ @rally.connect
15
+
16
+ story = @rally.find_story_with_id @args.first
17
+ opts = validate_and_fix_up_arguments_with_rally(story, opts)
18
+
19
+ story.update opts
20
+
21
+ puts "#{story.formatted_id}: #{story.name} (#{story.state})"
22
+ end
23
+
24
+ def validate_and_fix_up_arguments
25
+ Trollop::die "No story given" if @args.empty?
26
+ Trollop::die "Nothing to update. Please provide some options" unless @cmd_opts.any? {|k, v| k.to_s =~ /_given$/ }
27
+
28
+ opts = {}
29
+ opts[:name] = @cmd_opts[:name] if @cmd_opts[:name_given]
30
+ opts[:schedule_state] = state_from(@cmd_opts[:state]) if @cmd_opts[:state_given]
31
+
32
+ if @cmd_opts[:estimate_given]
33
+ opts[:plan_estimate] = @cmd_opts[:estimate] # nobody is going to remember "Plan Estimate", really
34
+ end
35
+
36
+ opts[:blocked] = @cmd_opts[:blocked] if @cmd_opts[:blocked_given]
37
+ opts[:blocked] = !@cmd_opts[:unblocked] if @cmd_opts[:unblocked_given]
38
+
39
+ opts
40
+ end
41
+
42
+ def validate_and_fix_up_arguments_with_rally(story, opts)
43
+ if @cmd_opts[:iteration_given]
44
+ opts[:iteration] = @rally.find_iteration_by_name @cmd_opts[:iteration]
45
+ Trollop::die "Unknown iteration \"#{@cmd_opts[:iteration]}\"" if opts[:iteration].nil?
46
+ end
47
+
48
+ if @cmd_opts[:release_given]
49
+ opts[:release] = @rally.find_release_by_name @cmd_opts[:release]
50
+ Trollop::die "Unknown release \"#{@cmd_opts[:release]}\"" if opts[:release].nil?
51
+ end
52
+
53
+ if @cmd_opts[:parent_given]
54
+ opts[:parent] = @rally.find_story_with_id(@cmd_opts[:parent]).rally_object
55
+ Trollop::die "Unknown story \"#{@cmd_opts[:parent]}\"" if opts[:parent].nil?
56
+ end
57
+
58
+ opts[:name] = story.name if opts[:name].blank?
59
+ opts
60
+ end
61
+
62
+ def state_from(option)
63
+ fixed_option = option.gsub(/(^\w|[-_]\w)/) { $1.upcase.gsub(/_/, '-') }
64
+ Trollop::die :state, "has an invalid value" unless Crab::Story::VALID_STATES.include? fixed_option
65
+ fixed_option
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,18 @@
1
+ module Crab
2
+ module Utilities
3
+ def credentials_file
4
+ File.expand_path("~/.rally_credentials")
5
+ end
6
+
7
+ def valid_credentials_file
8
+ Trollop::die "Please log in first" unless File.exists? credentials_file
9
+ credentials_file
10
+ end
11
+
12
+ def valid_project_name(cmd_opts)
13
+ project_name = cmd_opts[:project_given] ? cmd_opts[:project] : Crab::Project.current_project_name
14
+ Trollop::die :project, "must be specified" if project_name.blank?
15
+ project_name
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ module Crab
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1 @@
1
+ ./crab/Gemfile.lock
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'rally_rest_api'
4
+ gem 'cucumber'
5
+ gem 'rake'
6
+
7
+ gem 'highline', :require => 'highline/import' # password prompts, etc
8
+ gem 'pry' # irb-like awesomeness for debugging
9
+ gem 'activesupport', :require => 'active_support/all' # nice additions to Ruby API
10
+ gem 'i18n'
11
+
12
+ gem 'mustache' # for file templates
13
+ gem 'sanitize' # clean up description HTML
14
+
@@ -0,0 +1,277 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ Bundler.require :default
5
+
6
+ task :ensure_credentials_are_present do
7
+ ENV['RALLY_USERNAME'] ||= ask('Username: ')
8
+ ENV['RALLY_PASSWORD'] ||= ask('Password: ') {|q| q.echo = "*" }
9
+ end
10
+
11
+ task :rally => :ensure_credentials_are_present do
12
+ @rally = RallyRestAPI.new :username => ENV['RALLY_USERNAME'], :password => ENV['RALLY_PASSWORD']
13
+ puts "Logged in as #{@rally.user.login_name}"
14
+ end
15
+
16
+ # a few things in this script break / do the wrong thing if you don't have
17
+ # the project set up or don't have access to it
18
+ task :project => :rally do
19
+ project = ENV['RALLY_PROJECT'] ||= ask('Project: ')
20
+ @project = @rally.find(:project) { equal :name, ENV['RALLY_PROJECT']}.first
21
+ puts "Using project \"#{ENV['RALLY_PROJECT']}\""
22
+ end
23
+
24
+ task :default => :rally do
25
+ binding.pry
26
+ end
27
+
28
+ class Feature < Mustache
29
+
30
+ self.template_path = File.join(File.dirname(__FILE__), 'templates')
31
+
32
+ # didn't like what's being done to support multiple languages
33
+ # this is going to break horribly when we try to add supoort for generating the
34
+ # scenarios from Rally and the whole roundtripping thing
35
+ # (as it'll be difficult to use partials, for example)
36
+ def initialize(rally_story, language=(ENV['CUCUMBER_LANG'] || ''))
37
+ @delegate = rally_story
38
+ self.class.template_file = File.join(self.class.template_path, "feature-#{language}.mustache") if language.present?
39
+ end
40
+
41
+ def name
42
+ @delegate.name
43
+ end
44
+
45
+ def formatted_id
46
+ @delegate.formatted_i_d
47
+ end
48
+
49
+ def state
50
+ (@delegate.schedule_state || "unknown").parameterize.underscore
51
+ end
52
+
53
+ def description
54
+ # this could use a lot of rethinking :(
55
+ # biggest problem is that Cucumber breaks if text in description looks like something
56
+ # it might know how to parse, but doesn't
57
+ # our feature descriptions are quite like that, so I was being ultra-conservative
58
+ sanitize(@delegate.description || '').gsub(/ +/, "\n").gsub(/\n\n/, "\n").gsub(/\n/, "\n ")
59
+ end
60
+
61
+ def file_name
62
+ "#{formatted_id}_#{name.parameterize.underscore}.feature"
63
+ end
64
+
65
+ def dir_name
66
+ state
67
+ end
68
+
69
+ end
70
+
71
+ task :generate_features => :project do
72
+ project = @project
73
+ @stories = @rally.find(:hierarchical_requirement, :fetch => true) { equal :project, project }
74
+
75
+ @stories.each do |story|
76
+ feature = Feature.new(story)
77
+
78
+ dir = File.join("features", feature.dir_name)
79
+ file = File.join(dir, feature.file_name)
80
+
81
+ FileUtils.mkdir_p dir
82
+ FileUtils.touch file
83
+
84
+ File.open(file, 'w') do |f|
85
+ f.write feature.render
86
+ end
87
+ putc '.'
88
+ end
89
+ puts
90
+
91
+ end
92
+ # this whole class should die and we should probably use
93
+ # a Cucumber formatter instead of relying directly on the Gherkin
94
+ # APIs. With the extra info that Cucumber provides, it should also
95
+ # be possible to generate Test Case Runs, which would be pretty sweet
96
+ # to track using charts in Rally -- eg "How often do I have to do a manual
97
+ # test run of the @manual stuff so it's all good?"
98
+ class FeatureProxy
99
+ def initialize
100
+ @scenarios = []
101
+ @steps = ActiveSupport::OrderedHash.new([])
102
+ end
103
+
104
+ attr_reader :scenarios, :steps
105
+
106
+ def name
107
+ @feature.name
108
+ end
109
+
110
+ def description
111
+ @feature.description
112
+ end
113
+
114
+ # the next two methods could be replaced with something like an extension to the cucumber syntax
115
+ # in which we annotate foreign IDs -- my proposal is something like what's in here:
116
+ #
117
+ # Feature: [XXXXX] Lorem Ipsum
118
+ #
119
+ # Simple regex, fairly hard to get wrong in almost any western keyboard, and should work for any bug/issue/card/story tracker I've seen
120
+ #
121
+ def rally_id
122
+ self.name.match(/^\[([^\]]+)\](.*)$/)
123
+ $1
124
+ end
125
+
126
+ def title_for_rally
127
+ self.name.match(/^\[([^\]]+)\](.*)$/)
128
+ $2.squish.titleize
129
+ end
130
+
131
+ # needed by Gherkin
132
+ def uri(uri)
133
+ end
134
+
135
+ def feature(feature)
136
+ @feature = feature
137
+ end
138
+
139
+ def get
140
+ @feature
141
+ end
142
+
143
+ def scenario(scenario)
144
+ @scenarios << scenario
145
+ end
146
+
147
+ def step(step)
148
+ @steps[@scenarios.last] += Array(step)
149
+ end
150
+
151
+ def eof
152
+ end
153
+
154
+ def has_story_id?
155
+ !!self.name.match(/^\[([^\]]+)\](.*)$/)
156
+ end
157
+ end
158
+
159
+ def parse(feature)
160
+ updater = FeatureProxy.new
161
+ parser = Gherkin::Parser::Parser.new(updater, false, "root", false)
162
+ parser.parse File.read(feature), feature, 0
163
+ updater
164
+ end
165
+
166
+ # took a while to figure out that we need to remove the CSS from inside embedded <style> tags!
167
+ # Rally uses some crazy rich text editor that I'd be soooooo happy to disable, somehow. Chrome Extension, perhaps?
168
+ def sanitize(source)
169
+ Sanitize.clean source, :remove_contents => %w{style}
170
+ end
171
+
172
+ task :update_features => :rally do
173
+ Dir['features/**/*.feature'].sort {|a,b| File.mtime(b) <=> File.mtime(a) }.each do |file|
174
+ feature = parse file
175
+ unless feature.has_story_id?
176
+ raise "Incompatible feature name: #{feature.name} in #{file} (needs to begin with a story number in square brackets)"
177
+ end
178
+
179
+ # TODO inline variables
180
+ feature_id = feature.rally_id
181
+ feature_name = feature.title_for_rally
182
+
183
+ story = @rally.find(:hierarchical_requirement) { equal :formatted_i_d, feature_id }.first
184
+
185
+ updates = {} # collect all updates first (room for double-checking and you only get to call Rally over the net once per item)
186
+
187
+ if story.name != feature_name
188
+ updates[:name] = feature_name
189
+ end
190
+
191
+ # matching the descriptions sucked -- Rally does some conversion to strip out HTML
192
+ rally_description = sanitize(story.description || '').gsub(/\s+/, ' ').strip
193
+ cuke_description = sanitize(feature.description || '').gsub(/\s+/, ' ').strip
194
+
195
+ if rally_description != cuke_description
196
+ formatted_description = (feature.description || '').strip.gsub(/\n/, '<br/>')
197
+ updates[:description] = formatted_description
198
+ end
199
+ #
200
+
201
+ if updates.empty?
202
+ puts "Nothing to do for #{feature_id} (story already up to date)"
203
+ else
204
+ story.update updates
205
+ puts "Updated #{feature_id}: #{story.name} (#{updates.keys.join(',')})"
206
+ end
207
+ end
208
+ end
209
+
210
+ task :update_scenarios => :project do
211
+ Dir['features/**/*.feature'].sort {|a,b| File.mtime(b) <=> File.mtime(a) }.each do |file|
212
+ feature = parse file
213
+ feature.steps.each do |scenario, steps|
214
+ create_test_case @project, feature, scenario, steps
215
+
216
+ # TODO make the scenarios in the Cucumber file reflect what's in Rally as well
217
+ #
218
+ # goal is to have roundtrip editing, but if not whatever is in Cucumber is "the truth"
219
+ end
220
+ end
221
+ end
222
+
223
+ TYPE_TAGS = %w{acceptance functional non-functional performance regression usability user_interface}
224
+ RISK_TAGS = %w{low_risk medium_risk high_risk}
225
+ PRIORITY_TAGS = %{useful important critical}
226
+ METHOD_TAGS = %{automated manual}
227
+
228
+ def create_test_case(project, feature, scenario, steps)
229
+ if !scenario.name.match /^\[([^\]]+)\](.*)$/
230
+ # uh oh, we have to create this scenario!
231
+ tags = scenario.tags.map {|t| t.name.gsub(/^@/, '') }
232
+
233
+ # could definitely use some cleaning up, but the defaults seem sensible so far
234
+ type_tag = (tags.find {|t| TYPE_TAGS.include? t } || 'acceptance').humanize
235
+ risk_tag = (tags.find {|t| RISK_TAGS.include? t } || 'medium_risk').gsub(/_risk/, '').humanize
236
+ priority_tag = (tags.find {|t| PRIORITY_TAGS.include? t } || 'important').humanize
237
+ method_tag = (tags.find {|t| METHOD_TAGS.include? t } || 'automated').humanize
238
+
239
+ story = @rally.find(:hierarchical_requirement) { equal :formatted_i_d, feature.rally_id }.first
240
+
241
+ options = {
242
+ :name => scenario.name,
243
+ :description => "Automatically updated by Cucumber, do not edit",
244
+ :type => type_tag,
245
+ :risk => risk_tag,
246
+ :priority => priority_tag,
247
+ :method => method_tag,
248
+ :pre_conditions => "N/A", # do scenarios have a header? BeforeScenario?
249
+ :post_conditions => "N/A", # ...and/or a footer? AfterScenario, perhaps?
250
+ :work_product => story,
251
+ :project => @project # linking testcase to story is not enough
252
+ }
253
+
254
+ test_case = @rally.create(:test_case, options) do |test_case|
255
+ steps.each_with_index do |step, i|
256
+ test_case_step = @rally.create(:test_case_step, :test_case => test_case, :index => i, :input => "#{step.keyword.strip} #{step.name.strip}")
257
+ end
258
+ end
259
+
260
+ puts "Created test case #{test_case.formatted_i_d} in story #{feature.rally_id}"
261
+ else
262
+ # TODO update scenario text in feature with ID from Rally
263
+ # then update other fields in Rally with our scenario fields / tags
264
+ end
265
+ end
266
+
267
+ desc "DANGER DANGER DANGER"
268
+ task :delete_all_test_cases => :project do
269
+ story_id = ENV['STORY'] ||= ask("Story #: ")
270
+ project = @project
271
+ story = @rally.find(:hierarchical_requirement) { equal :formatted_i_d, story_id }.first
272
+ test_cases = @rally.find(:test_case) { equal :work_product, story }
273
+
274
+ test_cases.each do |tc|
275
+ p tc.delete
276
+ end
277
+ end
@@ -0,0 +1,5 @@
1
+ # language: pt
2
+ @{{state}}
3
+ Funcionalidade: [{{formatted_id}}] {{{name}}}
4
+
5
+ {{{description}}}
@@ -0,0 +1,4 @@
1
+ @{{state}}
2
+ Feature: [{{formatted_id}}] {{{name}}}
3
+
4
+ {{{description}}}
metadata ADDED
@@ -0,0 +1,230 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crab
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Carlos Villela
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-09-27 00:00:00 -03:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: aruba
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: aruba
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: cucumber
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: rally_rest_api
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :runtime
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: highline
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :runtime
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: activesupport
93
+ prerelease: false
94
+ requirement: &id006 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 3
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :runtime
104
+ version_requirements: *id006
105
+ - !ruby/object:Gem::Dependency
106
+ name: i18n
107
+ prerelease: false
108
+ requirement: &id007 !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ hash: 3
114
+ segments:
115
+ - 0
116
+ version: "0"
117
+ type: :runtime
118
+ version_requirements: *id007
119
+ - !ruby/object:Gem::Dependency
120
+ name: sanitize
121
+ prerelease: false
122
+ requirement: &id008 !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ hash: 3
128
+ segments:
129
+ - 0
130
+ version: "0"
131
+ type: :runtime
132
+ version_requirements: *id008
133
+ - !ruby/object:Gem::Dependency
134
+ name: trollop
135
+ prerelease: false
136
+ requirement: &id009 !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ hash: 3
142
+ segments:
143
+ - 0
144
+ version: "0"
145
+ type: :runtime
146
+ version_requirements: *id009
147
+ description: CRaB is a bridge between Cucumber and Rally
148
+ email:
149
+ - cvillela@thoughtworks.com
150
+ executables:
151
+ - crab
152
+ extensions: []
153
+
154
+ extra_rdoc_files: []
155
+
156
+ files:
157
+ - .gitignore
158
+ - .rvmrc
159
+ - Gemfile
160
+ - Rakefile
161
+ - bin/crab
162
+ - crab.gemspec
163
+ - features/find-text-in-stories.feature
164
+ - features/list-from-rally.feature
165
+ - features/login-and-out-of-rally.feature
166
+ - features/project-selection.feature
167
+ - features/pull-from-rally-into-cucumber.feature
168
+ - features/show-from-rally.feature
169
+ - features/steps/rally_steps.rb
170
+ - features/subcommand-help.feature
171
+ - features/support/aruba.rb
172
+ - features/update-story-in-rally.feature
173
+ - lib/crab.rb
174
+ - lib/crab/cli.rb
175
+ - lib/crab/cucumber_feature.rb
176
+ - lib/crab/cucumber_scenario.rb
177
+ - lib/crab/find.rb
178
+ - lib/crab/list.rb
179
+ - lib/crab/login.rb
180
+ - lib/crab/project.rb
181
+ - lib/crab/pull.rb
182
+ - lib/crab/rally.rb
183
+ - lib/crab/scenario.rb
184
+ - lib/crab/show.rb
185
+ - lib/crab/story.rb
186
+ - lib/crab/update.rb
187
+ - lib/crab/utilities.rb
188
+ - lib/crab/version.rb
189
+ - old/.gitignore
190
+ - old/Gemfile
191
+ - old/Gemfile.lock
192
+ - old/Rakefile
193
+ - old/templates/feature-pt.mustache
194
+ - old/templates/feature.mustache
195
+ has_rdoc: true
196
+ homepage: ""
197
+ licenses: []
198
+
199
+ post_install_message:
200
+ rdoc_options: []
201
+
202
+ require_paths:
203
+ - lib
204
+ required_ruby_version: !ruby/object:Gem::Requirement
205
+ none: false
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ hash: 3
210
+ segments:
211
+ - 0
212
+ version: "0"
213
+ required_rubygems_version: !ruby/object:Gem::Requirement
214
+ none: false
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ hash: 3
219
+ segments:
220
+ - 0
221
+ version: "0"
222
+ requirements: []
223
+
224
+ rubyforge_project: crab
225
+ rubygems_version: 1.6.2
226
+ signing_key:
227
+ specification_version: 3
228
+ summary: Cucumber-Rally Bridge
229
+ test_files: []
230
+