crab 0.1.0

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,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
+