rallycat 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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