rallycat 0.3.1 → 0.4.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/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rallycat (0.3.1)
4
+ rallycat (0.4.0)
5
5
  builder
6
6
  nokogiri
7
7
  rally_rest_api
data/TODO CHANGED
@@ -1,6 +1,5 @@
1
1
  # Rallycat TODO
2
2
 
3
- * update help (use option_parser help or custom help)
4
3
  * todo time
5
4
  * set todo time
6
5
  * set notes on a task (-n overwrite, -N to append)
@@ -8,26 +7,21 @@
8
7
  * add update behavior for defects (maybe)
9
8
  * Slow as balls? Make nice wait messages
10
9
  * Add man page
11
- * Ordering of arguments matters? (rallycat update TA1234 -b silently fails)
12
10
  * Create .rallycatrc file on first run?
13
11
  * Listing stories
14
12
  * Per iteration
15
13
  * Per project (project configured in rc or passed in?)
16
14
  * AC-only story view
15
+ * consolidate spec styles
17
16
 
18
17
  * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
19
18
 
20
19
  ## Ideas
21
20
 
22
- US666 - My Story
23
- =================
21
+ rallycat list -s <search phrase> # do a contains lookup for an iteration
24
22
 
25
- ````
26
- rallycat update --complete --blocked [-cdpsb] -o "Adam Tanner" TA1932`
27
-
28
- rallycat update -p -o "Emilio Cavazos" TA9832
29
- rallycat update -b TA1823 -m "Fix specs."
30
- ````
23
+ Keep a log of last (n) stories typed into `cat` so the default action for
24
+ `rallycat cat` will list the last (n) stories for the user.
31
25
 
32
26
  ### Notes
33
27
 
@@ -36,13 +30,12 @@ rallycat update -b TA1823 -m "Fix specs."
36
30
 
37
31
 
38
32
  ## Stats
33
+
39
34
  * Stories in progress
40
35
  * Stories accepted
41
36
  * Stories not accepted
42
37
  * Total To-Do Hours
43
38
 
44
-
45
-
46
39
  rally.vim -> Making User Stories from Vim
47
40
 
48
41
  ## Rallycat Futures
data/lib/rallycat.rb CHANGED
@@ -1,11 +1,20 @@
1
1
  require 'rallycat/cat'
2
2
  require 'rallycat/cli'
3
+ require 'rallycat/config'
3
4
  require 'rallycat/connection'
4
5
  require 'rallycat/help'
5
6
  require 'rallycat/html_to_text_converter'
7
+ require 'rallycat/list'
6
8
  require 'rallycat/update'
7
9
  require 'rallycat/version'
8
10
 
9
11
  module Rallycat
10
- # Your code goes here...
12
+ class RallycatError < StandardError; end
13
+ class StoryNotFound < RallycatError; end
14
+ class InvalidConfigError < RallycatError; end
15
+ class InvalidCredentialsError < RallycatError; end
16
+ class ProjectNotFound < RallycatError; end
17
+ class IterationNotFound < RallycatError; end
18
+ class UserNotFound < RallycatError; end
19
+ class TaskNotFound < RallycatError; end
11
20
  end
data/lib/rallycat/cat.rb CHANGED
@@ -2,7 +2,6 @@ require 'nokogiri'
2
2
 
3
3
  module Rallycat
4
4
  class Cat
5
- class StoryNotFound < StandardError; end
6
5
 
7
6
  def initialize(rally_api)
8
7
  @rally_api = rally_api
data/lib/rallycat/cli.rb CHANGED
@@ -12,19 +12,66 @@ module Rallycat
12
12
  parse_command_options!
13
13
  end
14
14
 
15
+ def run
16
+ case @command
17
+ when 'cat'
18
+ api = Rallycat::Connection.new(options[:username], options[:password]).api
19
+
20
+ story_number = @argv.shift
21
+
22
+ abort 'The "cat" command requires a story or defect number.' unless story_number
23
+
24
+ @stdout.puts Rallycat::Cat.new(api).story(story_number)
25
+ when 'update'
26
+ api = Rallycat::Connection.new(options[:username], options[:password]).api
27
+
28
+ task_number = @argv.shift
29
+
30
+ abort 'The "update" command requires a task number.' unless task_number
31
+
32
+ opts = {}
33
+ opts[:blocked] = true if options[:blocked]
34
+ opts[:state] = "In-Progress" if options[:in_progress]
35
+ opts[:state] = "Completed" if options[:completed]
36
+ opts[:state] = "Defined" if options[:defined]
37
+ opts[:owner] = options[:owner] if options[:owner]
38
+
39
+ @stdout.puts Rallycat::Update.new(api).task(task_number, opts)
40
+ when 'list'
41
+ api = Rallycat::Connection.new(options[:username], options[:password]).api
42
+ project = options[:project]
43
+
44
+ if options[:iteration]
45
+ @stdout.puts Rallycat::List.new(api).stories(project, options[:iteration])
46
+ else
47
+ @stdout.puts Rallycat::List.new(api).iterations(project)
48
+ end
49
+ when 'help'
50
+ # `puts` calls `to_s`
51
+ @stdout.puts Rallycat::Help.new
52
+ else
53
+ @stdout.puts "'#{@command}' is not a supported command. See 'rallycat help'."
54
+ end
55
+
56
+ rescue Rallycat::RallycatError, ArgumentError => e
57
+ abort e.message
58
+ end
59
+
60
+ private
61
+
15
62
  def parse_global_options!
16
63
  @options ||= {}
17
64
 
18
65
  global = OptionParser.new do |opts|
19
- opts.on('-u USERNAME', '--username') do |username|
66
+ opts.on('-u USERNAME', '--username', 'Your Rally username') do |username|
20
67
  @options[:username] = username
21
68
  end
22
69
 
23
- opts.on('-p PASSWORD', '--password') do |password|
70
+ opts.on('-p PASSWORD', '--password', 'Your Rally password') do |password|
24
71
  @options[:password] = password
25
72
  end
26
73
 
27
- opts.on('-h', '--help') do
74
+ opts.on('-h', '--help', 'Show this message') do
28
75
  @stdout.puts Rallycat::Help.new
29
76
  exit
30
77
  end
@@ -37,78 +84,53 @@ module Rallycat
37
84
  @options ||= {}
38
85
 
39
86
  commands = {
40
- 'cat' => OptionParser.new,
87
+ 'cat' => OptionParser.new do |opts|
88
+ opts.banner = 'Usage: rallycat story <story number>'
89
+ end,
41
90
 
42
91
  'update' => OptionParser.new do |opts|
43
- opts.banner = 'Usage: rallycat update <story number> [options]'
92
+ opts.banner = 'Usage: rallycat update <task number> [options]'
44
93
 
45
- opts.on('-b', '--blocked') do |blocked|
94
+ opts.on('-b', '--blocked', 'Block the current state of a task') do |blocked|
46
95
  @options[:blocked] = true
47
96
  end
48
97
 
49
- opts.on('-p', '--in-progress') do |in_progress|
98
+ opts.on('-p', '--in-progress', 'Set the state of a task to "In-Progress"') do |in_progress|
50
99
  @options[:in_progress] = true
51
100
  end
52
101
 
53
- opts.on('-c', '--completed') do |completed|
102
+ opts.on('-c', '--completed', 'Set the state of a task to "Completed"') do |completed|
54
103
  @options[:completed] = true
55
104
  end
56
105
 
57
- opts.on('-d', '--defined') do |defined|
106
+ opts.on('-d', '--defined', 'Set the state of a task to "Defined"') do |defined|
58
107
  @options[:defined] = true
59
108
  end
60
109
 
61
- opts.on('-o OWNER', '--owner') do |owner|
110
+ opts.on('-o OWNER', '--owner', 'Set the owner of a task') do |owner|
62
111
  @options[:owner] = owner
63
112
  end
64
113
  end,
65
114
 
66
- 'help' => OptionParser.new
67
- }
115
+ 'list' => OptionParser.new do |opts|
116
+ opts.banner = 'Usage: rallycat list [options]'
68
117
 
69
- @command = @argv.shift
70
- commands[@command].parse! @argv if commands.has_key? @command
71
- end
72
-
73
- def run
74
- case @command
75
- when 'cat'
76
- api = Rallycat::Connection.new(options[:username], options[:password]).api
77
-
78
- story_number = @argv.shift
118
+ opts.on('-p [PROJECT]', '--project', 'The project whose iterations you want to list') do |project|
119
+ @options[:project] = project
120
+ end
79
121
 
80
- abort 'The "cat" command requires a story or defect number.' unless story_number
122
+ opts.on('-i ITERATION', '--iteration', 'The iteration whose stories you want to list') do |iteration|
123
+ @options[:iteration] = iteration
124
+ end
125
+ end,
81
126
 
82
- begin
83
- @stdout.puts Rallycat::Cat.new(api).story(story_number)
84
- rescue Rallycat::Cat::StoryNotFound => e
85
- abort e.message
127
+ 'help' => OptionParser.new do |opts|
128
+ opts.banner = 'Usage: rallycat help'
86
129
  end
87
- when 'update'
88
- api = Rallycat::Connection.new(options[:username], options[:password]).api
89
-
90
- task_number = @argv.shift
91
-
92
- abort 'The "update" command requires a task number.' unless task_number
93
-
94
- opts = {}
95
- opts[:blocked] = true if options[:blocked]
96
- opts[:state] = "In-Progress" if options[:in_progress]
97
- opts[:state] = "Completed" if options[:completed]
98
- opts[:state] = "Defined" if options[:defined]
99
- opts[:owner] = options[:owner] if options[:owner]
130
+ }
100
131
 
101
- begin
102
- @stdout.puts Rallycat::Update.new(api).task(task_number, opts)
103
- rescue Rallycat::Update::UserNotFound, Rallycat::Update::TaskNotFound => e
104
- abort e.message
105
- end
106
- when 'help'
107
- # `puts` calls `to_s`
108
- @stdout.puts Rallycat::Help.new
109
- else
110
- @stdout.puts "'#{@command}' is not a supported command. See 'rallycat help'."
111
- end
132
+ @command = @argv.shift
133
+ commands[@command].parse! @argv if commands.has_key? @command
112
134
  end
113
135
  end
114
136
  end
@@ -0,0 +1,16 @@
1
+ module Rallycat
2
+ class Config
3
+ def initialize
4
+ begin
5
+ @config = YAML.load_file File.expand_path('~/.rallycatrc')
6
+ rescue StandardError
7
+ message = "Your rallycat config file is missing or invalid. See 'rallycat help'."
8
+ raise InvalidConfigError.new message
9
+ end
10
+ end
11
+
12
+ def [](key)
13
+ @config[key]
14
+ end
15
+ end
16
+ end
@@ -2,46 +2,29 @@ require 'yaml'
2
2
  require 'rally_rest_api'
3
3
  require 'logger'
4
4
 
5
- module Rallycat
6
- class InvalidConfigError < StandardError; end
7
- class InvalidCredentialsError < StandardError; end
8
-
5
+ module Rallycat
9
6
  class Connection
10
7
  attr_reader :api
11
8
 
12
9
  def initialize(username=nil, password=nil)
13
- @username = username
14
- @password = password
10
+ @config = Config.new
15
11
 
16
- config = parse_config
12
+ username ||= @config['username']
13
+ password ||= @config['password']
17
14
 
18
15
  begin
16
+ logger = ENV['RALLY_DEBUG'] ? Logger.new(STDOUT) : Logger.new(nil)
17
+
19
18
  @api = RallyRestAPI.new \
20
19
  base_url: 'https://rally1.rallydev.com/slm',
21
- username: config.fetch('username'),
22
- password: config.fetch('password'),
23
- logger: Logger.new(nil)
20
+ username: username,
21
+ password: password,
22
+ logger: logger
24
23
 
25
24
  rescue Rally::NotAuthenticatedError
26
25
  raise InvalidCredentialsError.new('Your Rally credentials are invalid.')
27
26
  end
28
27
  end
29
-
30
- private
31
-
32
- def parse_config
33
- rc_file_path = File.expand_path '~/.rallycatrc'
34
-
35
- if @username || @password
36
- { 'username' => @username, 'password' => @password }
37
- else
38
- begin
39
- YAML.load_file(rc_file_path)
40
- rescue StandardError
41
- message = "Your rallycat config file is missing or invalid. Please RTFM."
42
- raise InvalidConfigError.new message
43
- end
44
- end
45
- end
46
28
  end
47
29
  end
30
+
data/lib/rallycat/help.rb CHANGED
@@ -2,27 +2,39 @@ module Rallycat
2
2
  class Help
3
3
  def to_s
4
4
  <<-HELP
5
- Rallycat is a command line utility for interacting with Rally.
5
+ Rallycat is a command-line utility for interacting with Rally. It should be
6
+ used to support a command-line centric workflow. Rallycat has the simple goal
7
+ of being a lightweight alternative to Rally's web interface. It provides quick
8
+ access to Rally features that are important to developers in their day-to-day
9
+ work.
6
10
 
7
11
  Configuration:
8
12
  Configuration is available through a ~/.rallycatrc file formatted as YAML.
9
13
  The file should have two keys: `username` and `password` for authenticating
10
- with the Rally API.
14
+ with the Rally API. An additional project attribute can be added to make some
15
+ commands easiser to use. Example:
16
+
17
+ username: email@host.com
18
+ password: pass1234
19
+ project: SuperBad
11
20
 
12
21
  Additionally, the `-u [USERNAME]` and `-p [PASSWORD]` flags may be provided
13
22
  at runtime and will take higher precedence than the configuration file.
14
23
 
15
24
  Global Options:
16
- -u [USERNAME] # The Rally user
17
- -p [PASSWORD] # The password for the Rally user
25
+ -u [USERNAME] # Your Rally username
26
+ -p [PASSWORD] # Your Rally password
18
27
  -h, --help # Displays this help text
19
28
 
20
29
  Commands:
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]
30
+ rallycat cat <story number> # Displays a user story or defect
31
+ rallycat update <task number> # Updates the state of a task
32
+ [--blocked | -b] [--in-progress | -p]
24
33
  [--completed | -c] [--defined | -d]
25
34
  [--owner | -o <fullname>]
35
+ rallycat list # List iterations and stories
36
+ [--project | -p <project name>]
37
+ [--iteration | -i <iteration name>]
26
38
  rallycat help # Displays this help text
27
39
 
28
40
 
@@ -0,0 +1,96 @@
1
+ module Rallycat
2
+ class List
3
+
4
+ def initialize(api)
5
+ @api = api
6
+ @config = Config.new
7
+ end
8
+
9
+ def iterations(project_name)
10
+ project_name ||= @config['project']
11
+
12
+ validate_arg project_name, 'Project name is required.'
13
+
14
+ project = find_project(project_name)
15
+
16
+ iterations = @api.find_all(:iteration, {
17
+ project: project,
18
+ order: 'StartDate desc',
19
+ pagesize: 10
20
+ }).results
21
+
22
+ if iterations.count == 0
23
+ return "No iterations could be found for project #{project_name}."
24
+ end
25
+
26
+ iteration_names = iterations.map(&:name).uniq
27
+
28
+ <<-LIST
29
+ # 5 Most recent iterations for "#{project_name}"
30
+
31
+ #{iteration_names.join("\n")}
32
+
33
+ LIST
34
+ end
35
+
36
+ def stories(project_name, iteration_name)
37
+ project_name ||= @config['project']
38
+
39
+ validate_arg project_name, 'Project name is required.'
40
+ validate_arg iteration_name, 'Iteration name is required.'
41
+
42
+ project = find_project(project_name)
43
+ iteration = find_iteration(iteration_name, project)
44
+
45
+ stories = @api.find(:hierarchical_requirement, {
46
+ project: project,
47
+ pagesize: 100,
48
+ fetch: true
49
+ }) { equal :iteration, iteration }.results
50
+
51
+ stories += @api.find(:defect, {
52
+ project: project,
53
+ pagesize: 100,
54
+ fetch: true
55
+ }) { equal :iteration, iteration }.results
56
+
57
+ if stories.count == 0
58
+ return %{No stories could be found for iteration "#{iteration_name}".}
59
+ end
60
+
61
+ list = %{# Stories for iteration "#{iteration_name}" - "#{project_name}"\n\n}
62
+
63
+ stories.each do |story|
64
+ state = story.schedule_state == 'In-Progress' ? 'P' : story.schedule_state[0]
65
+ list += "* [#{story.formatted_i_d}] [#{state}] #{story.name}\n"
66
+ end
67
+
68
+ list += "\n"
69
+ end
70
+
71
+ private
72
+
73
+ def validate_arg(val, message)
74
+ raise ArgumentError, message if val.nil? || val.empty?
75
+ end
76
+
77
+ def find_project(project_name)
78
+ project = @api.find(:project) { equal(:name, project_name) }.first
79
+
80
+ raise ProjectNotFound, "Project (#{project_name}) does not exist." unless project
81
+
82
+ project
83
+ end
84
+
85
+ def find_iteration(iteration_name, project)
86
+ iteration = @api.find(:iteration, :project => project) {
87
+ equal(:name, iteration_name)
88
+ }.first
89
+
90
+ raise IterationNotFound, "Iteration (#{iteration_name}) does not exist." unless iteration
91
+
92
+ iteration
93
+ end
94
+ end
95
+ end
96
+
@@ -1,7 +1,5 @@
1
1
  module Rallycat
2
2
  class Update
3
- class UserNotFound < StandardError; end
4
- class TaskNotFound < StandardError; end
5
3
 
6
4
  def initialize(api)
7
5
  @api = api
@@ -1,3 +1,3 @@
1
1
  module Rallycat
2
- VERSION = "0.3.1"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -57,7 +57,7 @@ describe 'Rallycat' do
57
57
  expected = string_io.read
58
58
 
59
59
  expected.should include("rallycat cat <story number>")
60
- expected.should include("Displays the user story")
60
+ expected.should include("Displays a user story or defect")
61
61
  end
62
62
  end
63
63
 
@@ -183,4 +183,108 @@ describe 'Rallycat' do
183
183
  end
184
184
  end
185
185
  end
186
+
187
+ context 'list' do
188
+
189
+ it 'outputs the last 5 iterations for the users configured project' do
190
+ sout = StringIO.new
191
+
192
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password list }, sout
193
+
194
+ Artifice.activate_with RallyIterationsResponder.new do
195
+ rc_file = File.expand_path("~/.rallycatrc")
196
+ YAML.stub(:load_file).with(rc_file).and_return({
197
+ 'project' => 'SuperBad'
198
+ })
199
+
200
+ cli.run
201
+ end
202
+
203
+ sout.rewind
204
+ sout.read.should include('# 5 Most recent iterations for "SuperBad"')
205
+ end
206
+
207
+ it 'outputs the last 5 iterations for the project provided' do
208
+ sout = StringIO.new
209
+
210
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password list -p SuperBad }, sout
211
+
212
+ Artifice.activate_with RallyIterationsResponder.new do
213
+ cli.run
214
+ end
215
+
216
+ sout.rewind
217
+ sout.read.should include('# 5 Most recent iterations for "SuperBad"')
218
+ end
219
+
220
+ it 'outputs the stories for an iteration constrained by the configured project' do
221
+ sout = StringIO.new
222
+
223
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password list -i 25\ (2012-05-01\ to\ 2012-05-05) }, sout
224
+
225
+ Artifice.activate_with RallyStoriesResponder.new do
226
+ rc_file = File.expand_path("~/.rallycatrc")
227
+ YAML.stub(:load_file).with(rc_file).and_return({
228
+ 'project' => 'SuperBad'
229
+ })
230
+
231
+ cli.run
232
+ end
233
+
234
+ sout.rewind
235
+ sout.read.should include('# Stories for iteration "25 (2012-05-01 to 2012-05-05)" - "SuperBad"')
236
+ end
237
+
238
+ it 'outputs the stories for an iteration constrained by the project provided' do
239
+ sout = StringIO.new
240
+
241
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password list -p SuperBad -i 25\ (2012-05-01\ to\ 2012-05-05) }, sout
242
+
243
+ Artifice.activate_with RallyStoriesResponder.new do
244
+ cli.run
245
+ end
246
+
247
+ sout.rewind
248
+ sout.read.should include('# Stories for iteration "25 (2012-05-01 to 2012-05-05)" - "SuperBad"')
249
+ end
250
+
251
+ it 'aborts when project is not found' do
252
+ sout = StringIO.new
253
+
254
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password list -p WebFarts -i 25\ (2012-05-01\ to\ 2012-05-05) }, sout
255
+
256
+ lambda {
257
+ Artifice.activate_with RallyStoriesResponder.new do
258
+ cli.run
259
+ end
260
+ }.should raise_error(SystemExit, 'Project (WebFarts) does not exist.')
261
+ end
262
+
263
+ it 'aborts when iteration is not found' do
264
+ sout = StringIO.new
265
+
266
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password list -p SuperBad -i Sprint\ 0 }, sout
267
+
268
+ lambda {
269
+ Artifice.activate_with RallyStoriesResponder.new do
270
+ cli.run
271
+ end
272
+ }.should raise_error(SystemExit, 'Iteration (Sprint 0) does not exist.')
273
+ end
274
+
275
+ it 'aborts when project is not configured or provided' do
276
+ sout = StringIO.new
277
+
278
+ cli = Rallycat::CLI.new %w{ -u foo.bar@rallycat.com -p password list }, sout
279
+
280
+ lambda {
281
+ Artifice.activate_with RallyStoriesResponder.new do
282
+ rc_file = File.expand_path("~/.rallycatrc")
283
+ YAML.stub(:load_file).with(rc_file).and_return({ })
284
+
285
+ cli.run
286
+ end
287
+ }.should raise_error(SystemExit, 'Project name is required.')
288
+ end
289
+ end
186
290
  end
@@ -148,7 +148,7 @@ STORY
148
148
 
149
149
  lambda {
150
150
  cat.story(story_num)
151
- }.should raise_error(Rallycat::Cat::StoryNotFound, 'Story (US9999) does not exist.')
151
+ }.should raise_error(Rallycat::StoryNotFound, 'Story (US9999) does not exist.')
152
152
  end
153
153
  end
154
154
  end
File without changes
@@ -23,7 +23,7 @@ describe Rallycat::Connection do
23
23
  @connection = Rallycat::Connection.new
24
24
  end
25
25
  }.should raise_error Rallycat::InvalidConfigError,
26
- 'Your rallycat config file is missing or invalid. Please RTFM.'
26
+ "Your rallycat config file is missing or invalid. See 'rallycat help'."
27
27
  end
28
28
 
29
29
  it "raises when the credentials are invalid" do
@@ -88,4 +88,4 @@ describe Rallycat::Connection do
88
88
  end
89
89
 
90
90
  end
91
- end
91
+ end
@@ -0,0 +1,170 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rallycat::List, '#iterations' do
4
+
5
+ before do
6
+ Artifice.activate_with RallyAuthResponder.new do
7
+ @api = Rallycat::Connection.new('foo.bar@rallycat.com', 'password').api
8
+ end
9
+ end
10
+
11
+ it 'returns last five iterations for the project provided' do
12
+ expected = <<-LIST
13
+ # 5 Most recent iterations for "SuperBad"
14
+
15
+ 25 (2012-05-01 to 2012-05-05)
16
+ 24 (2012-04-01 to 2012-04-05)
17
+ 23 (2012-03-01 to 2012-03-05)
18
+ 22 (2012-02-01 to 2012-02-05)
19
+ 21 (2012-01-01 to 2012-01-05)
20
+
21
+ LIST
22
+
23
+ Artifice.activate_with RallyIterationsResponder.new do
24
+ project = 'SuperBad'
25
+ list = Rallycat::List.new(@api)
26
+ list.iterations(project).should == expected
27
+ end
28
+ end
29
+
30
+ it 'returns last five iterations for the configured project in .rallycatrc' do
31
+ expected = <<-LIST
32
+ # 5 Most recent iterations for "SuperBad"
33
+
34
+ 25 (2012-05-01 to 2012-05-05)
35
+ 24 (2012-04-01 to 2012-04-05)
36
+ 23 (2012-03-01 to 2012-03-05)
37
+ 22 (2012-02-01 to 2012-02-05)
38
+ 21 (2012-01-01 to 2012-01-05)
39
+
40
+ LIST
41
+
42
+ Artifice.activate_with RallyIterationsResponder.new do
43
+ rc_file = File.expand_path("~/.rallycatrc")
44
+ YAML.stub(:load_file).with(rc_file).and_return({
45
+ 'project' => 'SuperBad'
46
+ })
47
+ list = Rallycat::List.new(@api)
48
+ list.iterations(nil).should == expected
49
+ end
50
+ end
51
+
52
+ it 'raises when project name is empty' do
53
+ lambda {
54
+ list = Rallycat::List.new(@api)
55
+ list.iterations('').should == expected
56
+ }.should raise_error(ArgumentError, 'Project name is required.')
57
+ end
58
+
59
+ it 'raises when project name is nil' do
60
+ lambda {
61
+ rc_file = File.expand_path("~/.rallycatrc")
62
+ YAML.stub(:load_file).with(rc_file).and_return({})
63
+
64
+ list = Rallycat::List.new(@api)
65
+ list.iterations(nil).should == expected
66
+ }.should raise_error(ArgumentError, 'Project name is required.')
67
+ end
68
+
69
+ it 'raises when project cannot be found' do
70
+ Artifice.activate_with RallyNoResultsResponder.new do
71
+ lambda {
72
+ list = Rallycat::List.new(@api)
73
+ list.iterations('WebFarts').should == expected
74
+ }.should raise_error(Rallycat::ProjectNotFound, 'Project (WebFarts) does not exist.')
75
+ end
76
+ end
77
+
78
+ it 'returns a user friendly message when project has no iterations' do
79
+ Artifice.activate_with RallyIterationsResponder.new do
80
+ project = 'SuperAwful'
81
+ list = Rallycat::List.new(@api)
82
+ list.iterations(project).should == 'No iterations could be found for project SuperAwful.'
83
+ end
84
+ end
85
+ end
86
+
87
+ describe Rallycat::List, '#stories' do
88
+ before do
89
+ Artifice.activate_with RallyAuthResponder.new do
90
+ @api = Rallycat::Connection.new('foo.bar@rallycat.com', 'password').api
91
+ end
92
+ end
93
+
94
+ it 'returns the stories for an iteration filtered by sprint' do
95
+ expected = <<-LIST
96
+ # Stories for iteration "25 (2012-05-01 to 2012-05-05)" - "SuperBad"
97
+
98
+ * [US123] [C] This is story number one
99
+ * [US456] [P] This is story number two
100
+ * [DE789] [D] This is defect number one
101
+
102
+ LIST
103
+
104
+ Artifice.activate_with RallyStoriesResponder.new do
105
+ project = 'SuperBad'
106
+ iteration = '25 (2012-05-01 to 2012-05-05)'
107
+ list = Rallycat::List.new(@api)
108
+ list.stories(project, iteration).should == expected
109
+ end
110
+ end
111
+
112
+ it 'raises when project name is empty' do
113
+ lambda {
114
+ list = Rallycat::List.new(@api)
115
+ list.stories('', '25 (2012-05-01 to 2012-05-05)').should == expected
116
+ }.should raise_error(ArgumentError, 'Project name is required.')
117
+ end
118
+
119
+ it 'raises when project name is nil' do
120
+ lambda {
121
+ rc_file = File.expand_path("~/.rallycatrc")
122
+ YAML.stub(:load_file).with(rc_file).and_return({})
123
+
124
+ list = Rallycat::List.new(@api)
125
+ list.stories(nil, '25 (2012-05-01 to 2012-05-05)').should == expected
126
+ }.should raise_error(ArgumentError, 'Project name is required.')
127
+ end
128
+
129
+ it 'raises when iteration name is empty' do
130
+ lambda {
131
+ list = Rallycat::List.new(@api)
132
+ list.stories('SuperBad', '').should == expected
133
+ }.should raise_error(ArgumentError, 'Iteration name is required.')
134
+ end
135
+
136
+ it 'raises when iteration name is nil' do
137
+ lambda {
138
+ list = Rallycat::List.new(@api)
139
+ list.stories('SuperBad', nil).should == expected
140
+ }.should raise_error(ArgumentError, 'Iteration name is required.')
141
+ end
142
+
143
+ it 'raises when project cannot be found' do
144
+ Artifice.activate_with RallyNoResultsResponder.new do
145
+ lambda {
146
+ list = Rallycat::List.new(@api)
147
+ list.stories('WebFarts', '25 (2012-05-01 to 2012-05-05)')
148
+ }.should raise_error(Rallycat::ProjectNotFound, 'Project (WebFarts) does not exist.')
149
+ end
150
+ end
151
+
152
+ it 'raises when iteration cannot be found' do
153
+ Artifice.activate_with RallyStoriesResponder.new do
154
+ lambda {
155
+ list = Rallycat::List.new(@api)
156
+ list.stories('SuperBad', 'Sprint 0')
157
+ }.should raise_error(Rallycat::IterationNotFound, 'Iteration (Sprint 0) does not exist.')
158
+ end
159
+ end
160
+
161
+ it 'returns a user friendly message when iteration has no stories' do
162
+ Artifice.activate_with RallyStoriesResponder.new do
163
+ project = 'SuperBad'
164
+ iteration = '26 (2012-06-01 to 2012-06-05)'
165
+ list = Rallycat::List.new(@api)
166
+ list.stories(project, iteration).should == 'No stories could be found for iteration "26 (2012-06-01 to 2012-06-05)".'
167
+ end
168
+ end
169
+ end
170
+
@@ -119,7 +119,7 @@ describe Rallycat::Update do
119
119
 
120
120
  lambda {
121
121
  update.task(task_num, state: 'Completed', owner: 'Norman Notreal')
122
- }.should raise_error(Rallycat::Update::UserNotFound, 'User (Norman Notreal) does not exist.')
122
+ }.should raise_error(Rallycat::UserNotFound, 'User (Norman Notreal) does not exist.')
123
123
  end
124
124
  end
125
125
 
@@ -132,8 +132,8 @@ describe Rallycat::Update do
132
132
 
133
133
  lambda {
134
134
  update.task(task_num, state: 'Completed')
135
- }.should raise_error(Rallycat::Update::TaskNotFound, 'Task (TA6666) does not exist.')
135
+ }.should raise_error(Rallycat::TaskNotFound, 'Task (TA6666) does not exist.')
136
136
  end
137
137
 
138
138
  end
139
- end
139
+ end
@@ -10,6 +10,7 @@ class GenericResponder
10
10
  end
11
11
 
12
12
  def call(env)
13
+ # does not record the requests
13
14
  [200, {}, ['']]
14
15
  end
15
16
  end
@@ -0,0 +1,11 @@
1
+ class RallyAuthResponder < GenericResponder
2
+
3
+ def call(env)
4
+ @requests << request = Rack::Request.new(env)
5
+ if request.url == 'https://rally1.rallydev.com/slm/webservice/current/user'
6
+ [200, {}, ['<User><DisplayName></DisplayName></User>']]
7
+ else
8
+ [500, {}, ['oh noes']]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,76 @@
1
+ class RallyIterationsResponder < GenericResponder
2
+
3
+ def call(env)
4
+ @requests << request = Rack::Request.new(env)
5
+
6
+ case request.url
7
+ when 'https://rally1.rallydev.com/slm/webservice/current/Project?query=%28Name+%3D+SuperBad%29'
8
+ [200, {}, [
9
+ <<-XML
10
+ <QueryResult>
11
+ <Results>
12
+ <Object ref="https://rally1.rallydev.com/slm/webservice/1.36/project/777555" type="Project">
13
+ <Name>SuperBad</Name>
14
+ </Object>
15
+ </Results>
16
+ </QueryResult>
17
+ XML
18
+ ]]
19
+ when 'https://rally1.rallydev.com/slm/webservice/current/Iteration?query=%28ObjectID+%3E+0%29&project=https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.36%2Fproject%2F777555&order=Startdate+desc&pagesize=10'
20
+ [200, {}, [
21
+ <<-XML
22
+ <QueryResult>
23
+ <Results>
24
+ <Object type="Iteration">
25
+ <Name>25 (2012-05-01 to 2012-05-05)</Name>
26
+ </Object>
27
+ <Object type="Iteration">
28
+ <Name>25 (2012-05-01 to 2012-05-05)</Name>
29
+ </Object>
30
+ <Object type="Iteration">
31
+ <Name>24 (2012-04-01 to 2012-04-05)</Name>
32
+ </Object>
33
+ <Object type="Iteration">
34
+ <Name>24 (2012-04-01 to 2012-04-05)</Name>
35
+ </Object>
36
+ <Object type="Iteration">
37
+ <Name>23 (2012-03-01 to 2012-03-05)</Name>
38
+ </Object>
39
+ <Object type="Iteration">
40
+ <Name>23 (2012-03-01 to 2012-03-05)</Name>
41
+ </Object>
42
+ <Object type="Iteration">
43
+ <Name>22 (2012-02-01 to 2012-02-05)</Name>
44
+ </Object>
45
+ <Object type="Iteration">
46
+ <Name>22 (2012-02-01 to 2012-02-05)</Name>
47
+ </Object>
48
+ <Object type="Iteration">
49
+ <Name>21 (2012-01-01 to 2012-01-05)</Name>
50
+ </Object>
51
+ <Object type="Iteration">
52
+ <Name>21 (2012-01-01 to 2012-01-05)</Name>
53
+ </Object>
54
+ </Results>
55
+ </QueryResult>
56
+ XML
57
+ ]]
58
+ when 'https://rally1.rallydev.com/slm/webservice/current/Project?query=%28Name+%3D+SuperAwful%29'
59
+ [200, {}, [
60
+ <<-XML
61
+ <QueryResult>
62
+ <Results>
63
+ <Object ref="https://rally1.rallydev.com/slm/webservice/1.36/project/888444" type="Project">
64
+ <Name>SuperAwful</Name>
65
+ </Object>
66
+ </Results>
67
+ </QueryResult>
68
+ XML
69
+ ]]
70
+ when 'https://rally1.rallydev.com/slm/webservice/current/Iteration?query=%28ObjectID+%3E+0%29&project=https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.36%2Fproject%2F888444&order=Startdate+desc&pagesize=10'
71
+ RallyNoResultsResponder.new.call(env)
72
+ else
73
+ RallyNoResultsResponder.new.call(env)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,100 @@
1
+ class RallyStoriesResponder < GenericResponder
2
+
3
+ def call(env)
4
+ @requests << request = Rack::Request.new(env)
5
+
6
+ case request.url
7
+ when 'https://rally1.rallydev.com/slm/webservice/current/Project?query=%28Name+%3D+SuperBad%29'
8
+ [200, {}, [
9
+ <<-XML
10
+ <QueryResult>
11
+ <Results>
12
+ <Object ref="https://rally1.rallydev.com/slm/webservice/1.17/project/777555" type="Project" />
13
+ </Results>
14
+ </QueryResult>
15
+ XML
16
+ ]]
17
+ when 'https://rally1.rallydev.com/slm/webservice/1.17/project/777555'
18
+ [200, {}, [
19
+ <<-XML
20
+ <Project ref="https://rally1.rallydev.com/slm/webservice/1.17/project/777555">
21
+ <Name>SuperBad</Name>
22
+ </Project>
23
+ XML
24
+ ]]
25
+ when 'https://rally1.rallydev.com/slm/webservice/current/Iteration?query=%28Name+%3D+%2225+%282012-05-01+to+2012-05-05%29%22%29&project=https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.17%2Fproject%2F777555'
26
+ [200, {}, [
27
+ # NOTE: Always getting two results back from Rally, not sure why.
28
+ <<-XML
29
+ <QueryResult>
30
+ <Results>
31
+ <Object ref="https://rally1.rallydev.com/slm/webservice/1.17/iteration/1234" type="Iteration" />
32
+ <Object ref="https://rally1.rallydev.com/slm/webservice/1.17/iteration/4567" type="Iteration" />
33
+ </Results>
34
+ </QueryResult>
35
+ XML
36
+ ]]
37
+ when 'https://rally1.rallydev.com/slm/webservice/1.17/iteration/1234'
38
+ [200, {}, [
39
+ <<-XML
40
+ <Iteration>
41
+ <Name>25 (2012-05-01 to 2012-05-05)</Name>
42
+ </Iteration>
43
+ XML
44
+ ]]
45
+ when 'https://rally1.rallydev.com/slm/webservice/current/HierarchicalRequirement?query=%28Iteration+%3D+%29&project=https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.17%2Fproject%2F777555&pagesize=100&fetch=true'
46
+ [200, {}, [
47
+ <<-XML
48
+ <QueryResult>
49
+ <Results>
50
+ <Object type="HierarchicalRequirement">
51
+ <FormattedID>US123</FormattedID>
52
+ <Name>This is story number one</Name>
53
+ <ScheduleState>Completed</ScheduleState>
54
+ </Object>
55
+ <Object type="HierarchicalRequirement">
56
+ <FormattedID>US456</FormattedID>
57
+ <Name>This is story number two</Name>
58
+ <ScheduleState>In-Progress</ScheduleState>
59
+ </Object>
60
+ </Results>
61
+ </QueryResult>
62
+ XML
63
+ ]]
64
+ when 'https://rally1.rallydev.com/slm/webservice/current/Defect?query=%28Iteration+%3D+%29&project=https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.17%2Fproject%2F777555&pagesize=100&fetch=true'
65
+ [200, {}, [
66
+ <<-XML
67
+ <QueryResult>
68
+ <Results>
69
+ <Object type="Defect">
70
+ <FormattedID>DE789</FormattedID>
71
+ <Name>This is defect number one</Name>
72
+ <ScheduleState>Defined</ScheduleState>
73
+ </Object>
74
+ </Results>
75
+ </QueryResult>
76
+ XML
77
+ ]]
78
+
79
+ # These are for testing the not so happy path.
80
+ when 'https://rally1.rallydev.com/slm/webservice/current/Iteration?query=%28Name+%3D+%22Sprint+0%22%29&project=https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.17%2Fproject%2F777555'
81
+ RallyNoResultsResponder.new.call(env)
82
+ when 'https://rally1.rallydev.com/slm/webservice/current/Iteration?query=%28Name+%3D+%2226+%282012-06-01+to+2012-06-05%29%22%29&project=https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.17%2Fproject%2F777555'
83
+ [200, {}, [
84
+ <<-XML
85
+ <QueryResult>
86
+ <Results>
87
+ <Object ref="https://rally1.rallydev.com/slm/webservice/1.17/iteration/7890" type="Iteration" />
88
+ </Results>
89
+ </QueryResult>
90
+ XML
91
+ ]]
92
+ when 'https://rally1.rallydev.com/slm/webservice/1.17/iteration/7890'
93
+ # does nothing WTF(but will make tests fail if not here)???
94
+ when 'https://rally1.rallydev.com/slm/webservice/current/HierarchicalRequirement?query=%28Iteration+%3D+https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.17%2Fiteration%2F7890%29&project=https%3A%2F%2Frally1.rallydev.com%2Fslm%2Fwebservice%2F1.17%2Fproject%2F777555&pagesize=100&fetch=true'
95
+ RallyNoResultsResponder.new.call(env)
96
+ else
97
+ RallyNoResultsResponder.new.call(env)
98
+ end
99
+ end
100
+ 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.3.1
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,11 +10,11 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-07-19 00:00:00.000000000 Z
13
+ date: 2012-07-26 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: builder
17
- requirement: !ruby/object:Gem::Requirement
17
+ requirement: &70357622298460 !ruby/object:Gem::Requirement
18
18
  none: false
19
19
  requirements:
20
20
  - - ! '>='
@@ -22,15 +22,10 @@ dependencies:
22
22
  version: '0'
23
23
  type: :runtime
24
24
  prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
- none: false
27
- requirements:
28
- - - ! '>='
29
- - !ruby/object:Gem::Version
30
- version: '0'
25
+ version_requirements: *70357622298460
31
26
  - !ruby/object:Gem::Dependency
32
27
  name: rally_rest_api
33
- requirement: !ruby/object:Gem::Requirement
28
+ requirement: &70357622297960 !ruby/object:Gem::Requirement
34
29
  none: false
35
30
  requirements:
36
31
  - - ! '>='
@@ -38,15 +33,10 @@ dependencies:
38
33
  version: '0'
39
34
  type: :runtime
40
35
  prerelease: false
41
- version_requirements: !ruby/object:Gem::Requirement
42
- none: false
43
- requirements:
44
- - - ! '>='
45
- - !ruby/object:Gem::Version
46
- version: '0'
36
+ version_requirements: *70357622297960
47
37
  - !ruby/object:Gem::Dependency
48
38
  name: nokogiri
49
- requirement: !ruby/object:Gem::Requirement
39
+ requirement: &70357622297540 !ruby/object:Gem::Requirement
50
40
  none: false
51
41
  requirements:
52
42
  - - ! '>='
@@ -54,12 +44,7 @@ dependencies:
54
44
  version: '0'
55
45
  type: :runtime
56
46
  prerelease: false
57
- version_requirements: !ruby/object:Gem::Requirement
58
- none: false
59
- requirements:
60
- - - ! '>='
61
- - !ruby/object:Gem::Version
62
- version: '0'
47
+ version_requirements: *70357622297540
63
48
  description: The Rally website sucks. CLI is better.
64
49
  email:
65
50
  - adam@adamtanner.org
@@ -81,22 +66,28 @@ files:
81
66
  - lib/rallycat.rb
82
67
  - lib/rallycat/cat.rb
83
68
  - lib/rallycat/cli.rb
69
+ - lib/rallycat/config.rb
84
70
  - lib/rallycat/connection.rb
85
71
  - lib/rallycat/help.rb
86
72
  - lib/rallycat/html_to_text_converter.rb
73
+ - lib/rallycat/list.rb
87
74
  - lib/rallycat/update.rb
88
75
  - lib/rallycat/version.rb
89
76
  - rallycat.gemspec
90
77
  - spec/integration_spec.rb
91
- - spec/lib/rallycat/cat_spec.rb
92
- - spec/lib/rallycat/cli_spec.rb
93
- - spec/lib/rallycat/connection_spec.rb
94
- - spec/lib/rallycat/html_to_text_converter_spec.rb
95
- - spec/lib/rallycat/update_spec.rb
78
+ - spec/rallycat/cat_spec.rb
79
+ - spec/rallycat/cli_spec.rb
80
+ - spec/rallycat/connection_spec.rb
81
+ - spec/rallycat/html_to_text_converter_spec.rb
82
+ - spec/rallycat/list_spec.rb
83
+ - spec/rallycat/update_spec.rb
96
84
  - spec/spec_helper.rb
97
85
  - spec/support/generic_responder.rb
86
+ - spec/support/rally_auth_responder.rb
98
87
  - spec/support/rally_defect_responder.rb
88
+ - spec/support/rally_iterations_responder.rb
99
89
  - spec/support/rally_no_results_responder.rb
90
+ - spec/support/rally_stories_responder.rb
100
91
  - spec/support/rally_story_responder.rb
101
92
  - spec/support/rally_task_update_responder.rb
102
93
  homepage: https://github.com/adamtanner/rallycat
@@ -113,7 +104,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
113
104
  version: '0'
114
105
  segments:
115
106
  - 0
116
- hash: 1844620157454279745
107
+ hash: -2821747029306890735
117
108
  required_rubygems_version: !ruby/object:Gem::Requirement
118
109
  none: false
119
110
  requirements:
@@ -122,23 +113,27 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
113
  version: '0'
123
114
  segments:
124
115
  - 0
125
- hash: 1844620157454279745
116
+ hash: -2821747029306890735
126
117
  requirements: []
127
118
  rubyforge_project:
128
- rubygems_version: 1.8.23
119
+ rubygems_version: 1.8.7
129
120
  signing_key:
130
121
  specification_version: 3
131
122
  summary: The Rally website sucks. CLI is better.
132
123
  test_files:
133
124
  - spec/integration_spec.rb
134
- - spec/lib/rallycat/cat_spec.rb
135
- - spec/lib/rallycat/cli_spec.rb
136
- - spec/lib/rallycat/connection_spec.rb
137
- - spec/lib/rallycat/html_to_text_converter_spec.rb
138
- - spec/lib/rallycat/update_spec.rb
125
+ - spec/rallycat/cat_spec.rb
126
+ - spec/rallycat/cli_spec.rb
127
+ - spec/rallycat/connection_spec.rb
128
+ - spec/rallycat/html_to_text_converter_spec.rb
129
+ - spec/rallycat/list_spec.rb
130
+ - spec/rallycat/update_spec.rb
139
131
  - spec/spec_helper.rb
140
132
  - spec/support/generic_responder.rb
133
+ - spec/support/rally_auth_responder.rb
141
134
  - spec/support/rally_defect_responder.rb
135
+ - spec/support/rally_iterations_responder.rb
142
136
  - spec/support/rally_no_results_responder.rb
137
+ - spec/support/rally_stories_responder.rb
143
138
  - spec/support/rally_story_responder.rb
144
139
  - spec/support/rally_task_update_responder.rb