rallycat 0.2.0 → 0.3.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.
data/.rspec CHANGED
@@ -1,3 +1,3 @@
1
1
  --color
2
- --format progress
2
+ --format documentation
3
3
  --profile
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rallycat (0.2.0)
4
+ rallycat (0.3.0)
5
5
  builder
6
6
  nokogiri
7
7
  rally_rest_api
data/TODO CHANGED
@@ -1,14 +1,8 @@
1
1
  # Rallycat TODO
2
2
 
3
- * add update behavior for stories
4
- * add update behavior for defects
5
- * add sub-command support (so we can use -p for in-progress)
6
- * give user friendly error messages
7
- * when you don't pass in a task
8
- * when task does not exist
9
- * when owner cannot be found
10
- * update help
11
- * make "help" default output
3
+ * update help (use option_parser help or custom help)
4
+ * add update behavior for stories (maybe)
5
+ * add update behavior for defects (maybe)
12
6
  * Slow as balls? Make nice wait messages
13
7
  * Add man page
14
8
 
data/lib/rallycat/cat.rb CHANGED
@@ -2,6 +2,7 @@ require 'nokogiri'
2
2
 
3
3
  module Rallycat
4
4
  class Cat
5
+ class StoryNotFound < StandardError; end
5
6
 
6
7
  def initialize(rally_api)
7
8
  @rally_api = rally_api
@@ -14,7 +15,9 @@ module Rallycat
14
15
  equal :formatted_id, story_number
15
16
  end
16
17
 
17
- return "Story (#{ story_number }) does not exist." if results.total_result_count == 0
18
+ if results.total_result_count == 0
19
+ raise StoryNotFound, "Story (#{ story_number }) does not exist."
20
+ end
18
21
 
19
22
  consolidate_newlines parse_story(results.first)
20
23
  end
data/lib/rallycat/cli.rb CHANGED
@@ -9,7 +9,8 @@ module Rallycat
9
9
 
10
10
  def run
11
11
  options = {}
12
- option_parser = OptionParser.new do |opts|
12
+
13
+ global = OptionParser.new do |opts|
13
14
  opts.on('-u USERNAME', '--username') do |user|
14
15
  options[:user] = user
15
16
  end
@@ -18,40 +19,67 @@ module Rallycat
18
19
  options[:password] = password
19
20
  end
20
21
 
21
- opts.on('-b', '--blocked') do |blocked|
22
- options[:blocked] = true
22
+ opts.on('-h', '--help') do
23
+ @stdout.puts Rallycat::Help.new
24
+ exit
23
25
  end
26
+ end
24
27
 
25
- opts.on('-i', '--in-progress') do |in_progress|
26
- options[:in_progress] = true
27
- end
28
+ commands = {
29
+ 'cat' => OptionParser.new,
28
30
 
29
- opts.on('-c', '--completed') do |completed|
30
- options[:completed] = true
31
- end
31
+ 'update' => OptionParser.new do |opts|
32
+ opts.banner = 'Usage: rallycat update <story number> [options]'
32
33
 
33
- opts.on('-d', '--defined') do |defined|
34
- options[:defined] = true
35
- end
34
+ opts.on('-b', '--blocked') do |blocked|
35
+ options[:blocked] = true
36
+ end
36
37
 
37
- opts.on('-o OWNER', '--owner') do |owner|
38
- options[:owner] = owner
39
- end
40
- end
38
+ opts.on('-p', '--in-progress') do |in_progress|
39
+ options[:in_progress] = true
40
+ end
41
+
42
+ opts.on('-c', '--completed') do |completed|
43
+ options[:completed] = true
44
+ end
45
+
46
+ opts.on('-d', '--defined') do |defined|
47
+ options[:defined] = true
48
+ end
41
49
 
42
- option_parser.parse! @argv
50
+ opts.on('-o OWNER', '--owner') do |owner|
51
+ options[:owner] = owner
52
+ end
53
+ end,
43
54
 
44
- case @argv.shift
55
+ 'help' => OptionParser.new
56
+ }
57
+
58
+ global.order! @argv
59
+
60
+ command = @argv.shift
61
+ commands[command].order! @argv if commands.has_key? command
62
+
63
+ case command
45
64
  when 'cat'
46
65
  api = Rallycat::Connection.new(options[:user], options[:password]).api
47
66
 
48
- @stdout.puts Rallycat::Cat.new(api).story(@argv.shift)
67
+ story_number = @argv.shift
68
+
69
+ abort 'The "cat" command requires a story or defect number.' unless story_number
70
+
71
+ begin
72
+ @stdout.puts Rallycat::Cat.new(api).story(story_number)
73
+ rescue Rallycat::Cat::StoryNotFound => e
74
+ abort e.message
75
+ end
49
76
  when 'update'
50
77
  api = Rallycat::Connection.new(options[:user], options[:password]).api
51
78
 
52
-
53
79
  task_number = @argv.shift
54
80
 
81
+ abort 'The "update" command requires a task number.' unless task_number
82
+
55
83
  opts = {}
56
84
  opts[:blocked] = true if options[:blocked]
57
85
  opts[:state] = "In-Progress" if options[:in_progress]
@@ -59,12 +87,16 @@ module Rallycat
59
87
  opts[:state] = "Defined" if options[:defined]
60
88
  opts[:owner] = options[:owner] if options[:owner]
61
89
 
62
- @stdout.puts Rallycat::Update.new(api).task(task_number, opts)
90
+ begin
91
+ @stdout.puts Rallycat::Update.new(api).task(task_number, opts)
92
+ rescue Rallycat::Update::UserNotFound, Rallycat::Update::TaskNotFound => e
93
+ abort e.message
94
+ end
63
95
  when 'help'
64
96
  # `puts` calls `to_s`
65
97
  @stdout.puts Rallycat::Help.new
66
98
  else
67
- @stdout.puts 'only support for `cat` exists at the moment.'
99
+ @stdout.puts "'#{command}' is not a supported command. See 'rallycat help'."
68
100
  end
69
101
  end
70
102
  end
data/lib/rallycat/help.rb CHANGED
@@ -13,13 +13,17 @@ Configuration:
13
13
  at runtime and will take higher precedence than the configuration file.
14
14
 
15
15
  Global Options:
16
- -u [USERNAME] # The Rally user
17
- -p [PASSWORD] # The password for the Rally user
18
- -h, --help # Displays this help text
16
+ -u [USERNAME] # The Rally user
17
+ -p [PASSWORD] # The password for the Rally user
18
+ -h, --help # Displays this help text
19
19
 
20
20
  Commands:
21
- rallycat cat [STORY NUMBER] # Displays the user story
22
- rallycat help # Displays this help text
21
+ rallycat cat <story number> # Displays the user story or defect
22
+ rallycat update <task number> # Displays the user story
23
+ [--blocked | -b] [--in-progress | -i]
24
+ [--completed | -c] [--defined | -d]
25
+ [--owner | -o <fullname>]
26
+ rallycat help # Displays this help text
23
27
 
24
28
 
25
29
  HELP
@@ -1,38 +1,88 @@
1
1
  module Rallycat
2
2
  class Update
3
+ class UserNotFound < StandardError; end
4
+ class TaskNotFound < StandardError; end
5
+
3
6
  def initialize(api)
4
7
  @api = api
5
8
  end
6
9
 
7
10
  def task(task_number, attributes)
8
- results = @api.find(:task) do
9
- equal :formatted_id, task_number
10
- end
11
-
12
- task = results.first
11
+ task = find_task(task_number)
13
12
 
13
+ # When we set the state of the task and we don't explicitly set it to
14
+ # blocked we want to remove the block. In our workflow, when you change
15
+ # the state of the task it is also unblocked.
14
16
  if attributes[:state] && !attributes[:blocked]
15
17
  attributes[:blocked] = false
16
18
  end
17
19
 
18
- user_name = attributes[:owner]
19
-
20
- if user_name
21
- user_results = @api.find(:user) do
22
- equal :display_name, user_name
23
- end
24
- attributes[:owner] = user_results.first.login_name
20
+ # The value in attributes[:owner] should be equal to the desired owner's
21
+ # display name in Rally. We need to fetch the user in order to get their
22
+ # login_name. The login_name is needed in order to set the owner
23
+ # attribute of a task.
24
+ #
25
+ # We decided it would be easier for the user to enter 'John Smith'
26
+ # instead of 'john.smith@foobar.com'.
27
+ if display_name = attributes[:owner]
28
+ login_name = find_user(display_name)
29
+ attributes[:owner] = login_name
25
30
  end
26
31
 
27
32
  task.update(attributes)
28
33
 
29
34
  messages = []
30
35
 
31
- messages << %{Task (#{task_number}) was set to "#{attributes[:state]}".} if attributes[:state]
32
- messages << "Task (#{task_number}) was blocked." if attributes[:blocked]
33
- messages << %{Task (#{task_number}) was assigned to "#{user_name}".} if attributes[:owner]
36
+ if attributes[:state]
37
+ messages << state_message(task_number, attributes[:state])
38
+ end
39
+
40
+ if attributes[:blocked]
41
+ messages << blocked_message(task_number)
42
+ end
43
+
44
+ if attributes[:owner]
45
+ messages << owner_message(task_number, display_name)
46
+ end
34
47
 
35
48
  messages.join("\n")
36
49
  end
50
+
51
+ private
52
+ def find_task(task_number)
53
+ results = @api.find(:task) do
54
+ equal :formatted_id, task_number
55
+ end
56
+
57
+ if results.total_result_count == 0
58
+ raise TaskNotFound, "Task (#{task_number}) does not exist."
59
+ end
60
+
61
+ results.first
62
+ end
63
+
64
+ def find_user(display_name)
65
+ user_results = @api.find(:user) do
66
+ equal :display_name, display_name
67
+ end
68
+
69
+ if user_results.total_result_count == 0
70
+ raise UserNotFound, "User (#{display_name}) does not exist."
71
+ end
72
+
73
+ user_results.first.login_name
74
+ end
75
+
76
+ def state_message(task_number, state)
77
+ %{Task (#{task_number}) was set to "#{state}".}
78
+ end
79
+
80
+ def blocked_message(task_number)
81
+ "Task (#{task_number}) was blocked."
82
+ end
83
+
84
+ def owner_message(task_number, owner)
85
+ %{Task (#{task_number}) was assigned to "#{owner}".}
86
+ end
37
87
  end
38
88
  end
@@ -1,3 +1,3 @@
1
1
  module Rallycat
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -18,9 +18,34 @@ describe 'Rallycat' do
18
18
  sout.rewind
19
19
  sout.read.should include('# [US4567] - [Rework] Change link to button')
20
20
  end
21
+
22
+ it 'aborts when a story number is not given' do
23
+ sout = StringIO.new
24
+
25
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password cat }, sout
26
+
27
+ lambda {
28
+ Artifice.activate_with RallyStoryResponder.new do
29
+ cli.run
30
+ end
31
+ }.should raise_error(SystemExit, 'The "cat" command requires a story or defect number.')
32
+ end
33
+
34
+ it 'aborts when the story does not exist' do
35
+ sout = StringIO.new
36
+
37
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password cat US9999 }, sout
38
+
39
+ lambda {
40
+ Artifice.activate_with RallyNoResultsResponder.new do
41
+ cli.run
42
+ end
43
+ }.should raise_error(SystemExit, 'Story (US9999) does not exist.')
44
+ end
21
45
  end
22
46
 
23
47
  context 'help' do
48
+
24
49
  it 'displays a help screen to the user' do
25
50
  string_io = StringIO.new
26
51
 
@@ -31,7 +56,7 @@ describe 'Rallycat' do
31
56
  string_io.rewind
32
57
  expected = string_io.read
33
58
 
34
- expected.should include("rallycat cat [STORY NUMBER]")
59
+ expected.should include("rallycat cat <story number>")
35
60
  expected.should include("Displays the user story")
36
61
  end
37
62
  end
@@ -43,7 +68,7 @@ describe 'Rallycat' do
43
68
  it 'sets the state to in-progress' do
44
69
  sout = StringIO.new
45
70
 
46
- cli = Rallycat::CLI.new %w{ update -i TA6666 -u foo.bar@rallycat.com -p password }, sout
71
+ cli = Rallycat::CLI.new %w{ update -p TA6666 -u foo.bar@rallycat.com -p password }, sout
47
72
 
48
73
  task_responder = RallyTaskUpdateResponder.new
49
74
 
@@ -114,6 +139,48 @@ describe 'Rallycat' do
114
139
  sout.rewind
115
140
  sout.read.should include('Task (TA6666) was assigned to "Freddy Fender".')
116
141
  end
142
+
143
+ it 'aborts when the owner does not exist' do
144
+ sout = StringIO.new
145
+
146
+ cli = Rallycat::CLI.new %w{ update -o Norman\ Notreal TA6666 -u foo.bar@rallycat.com -p password }, sout
147
+
148
+ task_responder = RallyTaskUpdateResponder.new
149
+
150
+ lambda {
151
+ Artifice.activate_with task_responder do
152
+ cli.run
153
+ end
154
+ }.should raise_error(SystemExit, 'User (Norman Notreal) does not exist.')
155
+ end
156
+
157
+ it 'aborts when a task number is not given' do
158
+ sout = StringIO.new
159
+
160
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password update }, sout
161
+
162
+ task_responder = RallyTaskUpdateResponder.new
163
+
164
+ lambda {
165
+ Artifice.activate_with task_responder do
166
+ cli.run
167
+ end
168
+ }.should raise_error(SystemExit, 'The "update" command requires a task number.')
169
+ end
170
+
171
+ it 'aborts when a task does not exist' do
172
+ sout = StringIO.new
173
+
174
+ cli = Rallycat::CLI.new %w{ update -i TA9999 -u foo.bar@rallycat.com -p password }, sout
175
+
176
+ task_responder = RallyNoResultsResponder.new
177
+
178
+ lambda {
179
+ Artifice.activate_with task_responder do
180
+ cli.run
181
+ end
182
+ }.should raise_error(SystemExit, 'Task (TA9999) does not exist.')
183
+ end
117
184
  end
118
185
  end
119
186
  end
@@ -129,5 +129,30 @@ STORY
129
129
  end
130
130
  end
131
131
 
132
+ it 'raises when the story does not exist' do
133
+ responder = lambda do |env|
134
+ [200, {}, [
135
+ <<-XML
136
+ <QueryResult rallyAPIMajor="1" rallyAPIMinor="17">
137
+ <Errors/>
138
+ <Warnings/>
139
+ <TotalResultCount>0</TotalResultCount>
140
+ <StartIndex>1</StartIndex>
141
+ <PageSize>20</PageSize>
142
+ <Results/>
143
+ </QueryResult>
144
+ XML
145
+ ]]
146
+ end
147
+
148
+ Artifice.activate_with responder do
149
+ story_num = "US9999"
150
+ cat = Rallycat::Cat.new(@api)
151
+
152
+ lambda {
153
+ cat.story(story_num)
154
+ }.should raise_error(Rallycat::Cat::StoryNotFound, 'Story (US9999) does not exist.')
155
+ end
156
+ end
132
157
  end
133
158
 
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Rallycat::CLI do
4
4
  it 'should default to STDOUT' do
5
- STDOUT.should_receive(:puts).with 'only support for `cat` exists at the moment.'
5
+ STDOUT.should_receive(:puts).with "'' is not a supported command. See 'rallycat help'."
6
6
  cli = Rallycat::CLI.new []
7
7
  cli.run
8
8
  end
@@ -17,6 +17,6 @@ describe Rallycat::CLI, '#run' do
17
17
  cli.run
18
18
 
19
19
  sout.rewind
20
- sout.read.should == "only support for `cat` exists at the moment.\n"
20
+ sout.read.should == "'foo' is not a supported command. See 'rallycat help'.\n"
21
21
  end
22
22
  end
@@ -82,7 +82,6 @@ describe Rallycat::Update do
82
82
  message.should include('Task (TA6666) was assigned to "Freddy Fender"')
83
83
  end
84
84
 
85
- # require "pry"; binding.pry
86
85
  post_request = responder.requests[4] # this is the request that actually updates the task
87
86
  post_request.should be_post
88
87
  post_request.url.should == 'https://rally1.rallydev.com/slm/webservice/1.17/task/12345'
@@ -92,4 +91,31 @@ describe Rallycat::Update do
92
91
  body.should include('<Owner>fred.fender@testing.com</Owner>')
93
92
  end
94
93
  end
94
+
95
+ it 'raises when the user could not be found' do
96
+ responder = RallyTaskUpdateResponder.new
97
+
98
+ Artifice.activate_with responder do
99
+ task_num = "TA6666"
100
+ update = Rallycat::Update.new(@api)
101
+
102
+ lambda {
103
+ update.task(task_num, state: 'Completed', owner: 'Norman Notreal')
104
+ }.should raise_error(Rallycat::Update::UserNotFound, 'User (Norman Notreal) does not exist.')
105
+ end
106
+ end
107
+
108
+ it 'raises when the task could not be found' do
109
+ responder = RallyNoResultsResponder.new
110
+
111
+ Artifice.activate_with responder do
112
+ task_num = "TA6666"
113
+ update = Rallycat::Update.new(@api)
114
+
115
+ lambda {
116
+ update.task(task_num, state: 'Completed')
117
+ }.should raise_error(Rallycat::Update::TaskNotFound, 'Task (TA6666) does not exist.')
118
+ end
119
+
120
+ end
95
121
  end
@@ -0,0 +1,28 @@
1
+ class RallyNoResultsResponder
2
+ attr_reader :requests
3
+
4
+ def initialize
5
+ @requests = []
6
+ end
7
+
8
+ def last_request
9
+ @requests.last
10
+ end
11
+
12
+ def call(env)
13
+ @requests << request = Rack::Request.new(env)
14
+
15
+ [200, {}, [
16
+ <<-XML
17
+ <QueryResult rallyAPIMajor="1" rallyAPIMinor="17">
18
+ <Errors/>
19
+ <Warnings/>
20
+ <TotalResultCount>0</TotalResultCount>
21
+ <StartIndex>1</StartIndex>
22
+ <PageSize>20</PageSize>
23
+ <Results/>
24
+ </QueryResult>
25
+ XML
26
+ ]]
27
+ end
28
+ end
@@ -82,7 +82,7 @@ class RallyTaskUpdateResponder
82
82
  XML
83
83
  ]]
84
84
  else
85
- [200, {}, ['<else />']]
85
+ RallyNoResultsResponder.new.call(env)
86
86
  end
87
87
  end
88
88
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rallycat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -14,7 +14,7 @@ date: 2012-07-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: builder
17
- requirement: &70176499099720 !ruby/object:Gem::Requirement
17
+ requirement: &70303674111620 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,10 +22,10 @@ dependencies:
22
22
  version: '0'
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: *70176499099720
25
+ version_requirements: *70303674111620
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: rally_rest_api
28
- requirement: &70176499099160 !ruby/object:Gem::Requirement
28
+ requirement: &70303674111020 !ruby/object:Gem::Requirement
29
29
  none: false
30
30
  requirements:
31
31
  - - ! '>='
@@ -33,10 +33,10 @@ dependencies:
33
33
  version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
- version_requirements: *70176499099160
36
+ version_requirements: *70303674111020
37
37
  - !ruby/object:Gem::Dependency
38
38
  name: nokogiri
39
- requirement: &70176499098740 !ruby/object:Gem::Requirement
39
+ requirement: &70303674110340 !ruby/object:Gem::Requirement
40
40
  none: false
41
41
  requirements:
42
42
  - - ! '>='
@@ -44,7 +44,7 @@ dependencies:
44
44
  version: '0'
45
45
  type: :runtime
46
46
  prerelease: false
47
- version_requirements: *70176499098740
47
+ version_requirements: *70303674110340
48
48
  description: The Rally website sucks. CLI is better.
49
49
  email:
50
50
  - adam@adamtanner.org
@@ -80,6 +80,7 @@ files:
80
80
  - spec/lib/rallycat/update_spec.rb
81
81
  - spec/spec_helper.rb
82
82
  - spec/support/rally_defect_responder.rb
83
+ - spec/support/rally_no_results_responder.rb
83
84
  - spec/support/rally_story_responder.rb
84
85
  - spec/support/rally_task_update_responder.rb
85
86
  homepage: https://github.com/adamtanner/rallycat
@@ -96,7 +97,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
97
  version: '0'
97
98
  segments:
98
99
  - 0
99
- hash: 333992134023580695
100
+ hash: 4474919884476792167
100
101
  required_rubygems_version: !ruby/object:Gem::Requirement
101
102
  none: false
102
103
  requirements:
@@ -105,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
106
  version: '0'
106
107
  segments:
107
108
  - 0
108
- hash: 333992134023580695
109
+ hash: 4474919884476792167
109
110
  requirements: []
110
111
  rubyforge_project:
111
112
  rubygems_version: 1.8.7
@@ -121,5 +122,6 @@ test_files:
121
122
  - spec/lib/rallycat/update_spec.rb
122
123
  - spec/spec_helper.rb
123
124
  - spec/support/rally_defect_responder.rb
125
+ - spec/support/rally_no_results_responder.rb
124
126
  - spec/support/rally_story_responder.rb
125
127
  - spec/support/rally_task_update_responder.rb