rallycat 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +1 -1
- data/.rspec +3 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +44 -0
- data/Rakefile +7 -1
- data/TODO +50 -0
- data/bin/rallycat +2 -7
- data/lib/rallycat/cat.rb +55 -12
- data/lib/rallycat/cli.rb +35 -0
- data/lib/rallycat/connection.rb +44 -0
- data/lib/rallycat/html_to_text_converter.rb +56 -0
- data/lib/rallycat/version.rb +1 -1
- data/lib/rallycat.rb +5 -2
- data/rallycat.gemspec +6 -0
- data/spec/integration_spec.rb +30 -0
- data/spec/lib/rallycat/cat_spec.rb +133 -0
- data/spec/lib/rallycat/cli_spec.rb +22 -0
- data/spec/lib/rallycat/connection_spec.rb +91 -0
- data/spec/lib/rallycat/html_to_text_converter_spec.rb +78 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/rally_defect_responder.rb +42 -0
- data/spec/support/rally_story_responder.rb +91 -0
- metadata +61 -6
data/.gitignore
CHANGED
data/.rspec
ADDED
data/Gemfile
CHANGED
data/Gemfile.lock
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
rallycat (0.1.0)
|
5
|
+
builder
|
6
|
+
nokogiri
|
7
|
+
rally_rest_api
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
artifice (0.6)
|
13
|
+
rack-test
|
14
|
+
builder (3.0.0)
|
15
|
+
coderay (1.0.6)
|
16
|
+
diff-lcs (1.1.3)
|
17
|
+
method_source (0.7.1)
|
18
|
+
nokogiri (1.5.3)
|
19
|
+
pry (0.9.9.6)
|
20
|
+
coderay (~> 1.0.5)
|
21
|
+
method_source (~> 0.7.1)
|
22
|
+
slop (>= 2.4.4, < 3)
|
23
|
+
rack (1.4.1)
|
24
|
+
rack-test (0.6.1)
|
25
|
+
rack (>= 1.0)
|
26
|
+
rally_rest_api (1.0.3)
|
27
|
+
rspec (2.10.0)
|
28
|
+
rspec-core (~> 2.10.0)
|
29
|
+
rspec-expectations (~> 2.10.0)
|
30
|
+
rspec-mocks (~> 2.10.0)
|
31
|
+
rspec-core (2.10.1)
|
32
|
+
rspec-expectations (2.10.0)
|
33
|
+
diff-lcs (~> 1.1.3)
|
34
|
+
rspec-mocks (2.10.1)
|
35
|
+
slop (2.4.4)
|
36
|
+
|
37
|
+
PLATFORMS
|
38
|
+
ruby
|
39
|
+
|
40
|
+
DEPENDENCIES
|
41
|
+
artifice
|
42
|
+
pry
|
43
|
+
rallycat!
|
44
|
+
rspec
|
data/Rakefile
CHANGED
data/TODO
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# Rallycat TODO
|
2
|
+
|
3
|
+
* Slow as balls? Make nice wait messages
|
4
|
+
* Reconcile USER vs username (cli arg vs config attribute)
|
5
|
+
* Add help
|
6
|
+
* Add man page
|
7
|
+
* Integration specs
|
8
|
+
|
9
|
+
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
10
|
+
|
11
|
+
## Ideas
|
12
|
+
|
13
|
+
US666 - My Story
|
14
|
+
=================
|
15
|
+
|
16
|
+
````
|
17
|
+
rallycat update --complete --blocked [-cdpsb] -o "Adam Tanner" TA1932`
|
18
|
+
|
19
|
+
rallycat update -p -o "Emilio Cavazos" TA9832
|
20
|
+
rallycat update -b TA1823 -m "Fix specs."
|
21
|
+
````
|
22
|
+
|
23
|
+
### Notes
|
24
|
+
|
25
|
+
* Unblock by setting a new state
|
26
|
+
* Don't allow setting a new state and blocking, -b blocks current state
|
27
|
+
|
28
|
+
|
29
|
+
## Stats
|
30
|
+
* Stories in progress
|
31
|
+
* Stories accepted
|
32
|
+
* Stories not accepted
|
33
|
+
* Total To-Do Hours
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
rally.vim -> Making User Stories from Vim
|
38
|
+
|
39
|
+
## Rallycat Futures
|
40
|
+
|
41
|
+
cat tasks | rallycat boilerplate
|
42
|
+
|
43
|
+
[DESCRIPTION] [ESTIMATE] [TO-DO]
|
44
|
+
|
45
|
+
### Task Example
|
46
|
+
|
47
|
+
Show Andrew on frontpage ## 2 ## 2
|
48
|
+
Do some shit with Redis ## 3 ## 3
|
49
|
+
Redo everything ever ## 140 ## 140
|
50
|
+
|
data/bin/rallycat
CHANGED
data/lib/rallycat/cat.rb
CHANGED
@@ -1,26 +1,69 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
1
3
|
module Rallycat
|
2
4
|
class Cat
|
3
5
|
|
6
|
+
def initialize(rally_api)
|
7
|
+
@rally_api = rally_api
|
8
|
+
end
|
9
|
+
|
4
10
|
def story(story_number)
|
5
|
-
|
11
|
+
story_type = story_number.start_with?('US') ? :hierarchical_requirement : :defect
|
12
|
+
|
13
|
+
results = @rally_api.find(story_type, fetch: true) do
|
14
|
+
equal :formatted_id, story_number
|
15
|
+
end
|
16
|
+
|
17
|
+
return "Story (#{ story_number }) does not exist." if results.total_result_count == 0
|
18
|
+
|
19
|
+
consolidate_newlines parse_story(results.first)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
6
23
|
|
24
|
+
# NOTE: story.elements.keys => all properties exposed by rally
|
25
|
+
def parse_story(story)
|
26
|
+
<<-TEXT
|
7
27
|
|
8
|
-
|
9
|
-
## [#{story_number}] Rally Title
|
10
|
-
------------------------------------------------
|
28
|
+
# [#{story.formatted_i_d}] - #{story.name}
|
11
29
|
|
12
|
-
|
13
|
-
|
30
|
+
Plan Estimate: #{story.plan_estimate}
|
31
|
+
State: #{story.schedule_state}
|
32
|
+
Task Actual: #{story.task_actual_total}
|
33
|
+
Task Estimate: #{story.task_estimate_total}
|
34
|
+
Task Remaining: #{story.task_remaining_total}
|
35
|
+
Owner: #{story.owner}
|
14
36
|
|
15
|
-
|
16
|
-
* [TA1234] I should be able to go to a page
|
17
|
-
* [TA1234] I should be able to go to a page
|
37
|
+
## DESCRIPTION
|
18
38
|
|
39
|
+
#{HtmlToTextConverter.new(story.description).parse}
|
19
40
|
|
20
|
-
|
41
|
+
## TASKS
|
21
42
|
|
22
|
-
|
43
|
+
#{parse_tasks(story)}
|
44
|
+
|
45
|
+
TEXT
|
23
46
|
end
|
24
47
|
|
48
|
+
def parse_tasks(story)
|
49
|
+
return '' unless story.tasks
|
50
|
+
|
51
|
+
tasks = story.tasks
|
52
|
+
sorted_tasks = tasks.sort_by{ |t| t.task_index }
|
53
|
+
|
54
|
+
# This is an example of a task formatted in plain text:
|
55
|
+
#
|
56
|
+
# [TA12345] [C] The name of the task.
|
57
|
+
#
|
58
|
+
sorted_tasks.map do |task|
|
59
|
+
state = task.state == 'In-Progress' ? 'P' : task.state[0]
|
60
|
+
"[#{task.formatted_i_d}] [#{state}] #{task.name}"
|
61
|
+
end.join("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def consolidate_newlines(story)
|
65
|
+
story.gsub(/\n{3,}/, "\n\n")
|
66
|
+
end
|
25
67
|
end
|
26
|
-
end
|
68
|
+
end
|
69
|
+
|
data/lib/rallycat/cli.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Rallycat
|
4
|
+
class CLI
|
5
|
+
def initialize(argv, stdout=STDOUT)
|
6
|
+
@argv = argv
|
7
|
+
@stdout = stdout
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
options = {}
|
12
|
+
option_parser = OptionParser.new do |opts|
|
13
|
+
opts.on('-u USER') do |user|
|
14
|
+
options[:user] = user
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on('-p PASSWORD') do |password|
|
18
|
+
options[:password] = password
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
option_parser.parse! @argv
|
23
|
+
|
24
|
+
case @argv.shift
|
25
|
+
when 'cat'
|
26
|
+
api = Rallycat::Connection.new(options[:user], options[:password]).api
|
27
|
+
|
28
|
+
@stdout.puts Rallycat::Cat.new(api).story(@argv.shift)
|
29
|
+
else
|
30
|
+
@stdout.puts 'only support for `cat` exists at the moment.'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'rally_rest_api'
|
3
|
+
|
4
|
+
module Rallycat
|
5
|
+
class InvalidConfigError < StandardError; end
|
6
|
+
class InvalidCredentialsError < StandardError; end
|
7
|
+
|
8
|
+
class Connection
|
9
|
+
attr_reader :api
|
10
|
+
|
11
|
+
def initialize(username=nil, password=nil)
|
12
|
+
@username = username
|
13
|
+
@password = password
|
14
|
+
|
15
|
+
config = parse_config
|
16
|
+
|
17
|
+
begin
|
18
|
+
@api = RallyRestAPI.new \
|
19
|
+
base_url: 'https://rally1.rallydev.com/slm',
|
20
|
+
username: config.fetch('username'),
|
21
|
+
password: config.fetch('password')
|
22
|
+
rescue Rally::NotAuthenticatedError
|
23
|
+
raise InvalidCredentialsError.new('Your Rally credentials are invalid.')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def parse_config
|
30
|
+
rc_file_path = File.expand_path '~/.rallycatrc'
|
31
|
+
|
32
|
+
if @username || @password
|
33
|
+
{ 'username' => @username, 'password' => @password }
|
34
|
+
else
|
35
|
+
begin
|
36
|
+
YAML.load_file(rc_file_path)
|
37
|
+
rescue StandardError
|
38
|
+
message = "Your rallycat config file is missing or invalid. Please RTFM."
|
39
|
+
raise InvalidConfigError.new message
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class HtmlToTextConverter
|
4
|
+
|
5
|
+
def initialize(html_string)
|
6
|
+
@html_string = html_string
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse
|
10
|
+
return '' unless @html_string
|
11
|
+
|
12
|
+
html = pre_parse
|
13
|
+
|
14
|
+
@fragment = Nokogiri::HTML.fragment(html)
|
15
|
+
|
16
|
+
add_newline_after_block_elements
|
17
|
+
parse_whitespace
|
18
|
+
parse_lists
|
19
|
+
|
20
|
+
@fragment.content
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def pre_parse
|
26
|
+
html = @html_string.gsub(/>\s+</, '><') # remove whitespace between tags
|
27
|
+
html.gsub(' ', ' ') # replace nbsp with regular space
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_newline_after_block_elements
|
31
|
+
@fragment.css('br, p, div, ul, ol').each { |node| node.after("\n") }
|
32
|
+
end
|
33
|
+
|
34
|
+
def parse_whitespace
|
35
|
+
@fragment.xpath('.//text()').each do |node|
|
36
|
+
if node.content =~ /\S/ # has non-whitespace characters
|
37
|
+
node.content = node.content.squeeze(' ') # consolidate whitespace
|
38
|
+
node.content = node.content.lstrip
|
39
|
+
else
|
40
|
+
# remove nodes that are only whitespace
|
41
|
+
node.remove unless node.content.include?("\n")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_lists
|
47
|
+
# Make lists pretty:
|
48
|
+
# * list item 1
|
49
|
+
# * list item 2
|
50
|
+
# * list item 3
|
51
|
+
@fragment.css('li').each do |node|
|
52
|
+
node.content = " * #{node.content}\n"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
data/lib/rallycat/version.rb
CHANGED
data/lib/rallycat.rb
CHANGED
data/rallycat.gemspec
CHANGED
@@ -14,4 +14,10 @@ Gem::Specification.new do |gem|
|
|
14
14
|
gem.name = "rallycat"
|
15
15
|
gem.require_paths = ["lib"]
|
16
16
|
gem.version = Rallycat::VERSION
|
17
|
+
|
18
|
+
# rally depends on this but does not declare it as a dependency
|
19
|
+
gem.add_dependency 'builder'
|
20
|
+
gem.add_dependency 'rally_rest_api'
|
21
|
+
gem.add_dependency 'nokogiri'
|
22
|
+
|
17
23
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Rallycat' do
|
4
|
+
|
5
|
+
context 'cat' do
|
6
|
+
|
7
|
+
it 'fetches, parses and outputs the rally story requested by the user' do
|
8
|
+
auth_responder = lambda do |env|
|
9
|
+
# 'https://rally1.rallydev.com/slm/webservice/current/user'
|
10
|
+
[200, {}, ['<foo>bar</foo>']]
|
11
|
+
end
|
12
|
+
|
13
|
+
sout = StringIO.new
|
14
|
+
cli = nil
|
15
|
+
|
16
|
+
Artifice.activate_with auth_responder do
|
17
|
+
cli = Rallycat::CLI.new %w{ cat US4567 -u=foo.bar@rallycat.com -p=password'}, sout
|
18
|
+
end
|
19
|
+
|
20
|
+
story_responder = RallyStoryResponder.new
|
21
|
+
|
22
|
+
Artifice.activate_with story_responder.endpoint do
|
23
|
+
cli.run
|
24
|
+
end
|
25
|
+
|
26
|
+
sout.rewind
|
27
|
+
sout.read.should include('# [US4567] - [Rework] Change link to button')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
describe Rallycat::Cat, '#story' do
|
5
|
+
|
6
|
+
before do
|
7
|
+
responder = lambda do |env|
|
8
|
+
# 'https://rally1.rallydev.com/slm/webservice/current/user'
|
9
|
+
[200, {}, ['<foo>bar</foo>']]
|
10
|
+
end
|
11
|
+
|
12
|
+
Artifice.activate_with responder do
|
13
|
+
@api = Rallycat::Connection.new('foo.bar@rallycat.com', 'password').api
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'outputs the rally story' do
|
18
|
+
|
19
|
+
expected = <<-STORY
|
20
|
+
|
21
|
+
# [US4567] - [Rework] Change link to button
|
22
|
+
|
23
|
+
Plan Estimate: 1.0
|
24
|
+
State: In-Progress
|
25
|
+
Task Actual: 0.0
|
26
|
+
Task Estimate: 6.5
|
27
|
+
Task Remaining: 0.5
|
28
|
+
Owner: scootin@fruity.com
|
29
|
+
|
30
|
+
## DESCRIPTION
|
31
|
+
|
32
|
+
This is the story
|
33
|
+
|
34
|
+
* Remember to do this.
|
35
|
+
* And this too.
|
36
|
+
|
37
|
+
## TASKS
|
38
|
+
|
39
|
+
[TA1234] [C] Change link to button
|
40
|
+
[TA1235] [P] Add confirmation
|
41
|
+
[TA1236] [D] Code Review
|
42
|
+
[TA1237] [D] QA Test
|
43
|
+
|
44
|
+
STORY
|
45
|
+
|
46
|
+
responder = RallyStoryResponder.new
|
47
|
+
|
48
|
+
Artifice.activate_with responder.endpoint do
|
49
|
+
story_num = 'US7176'
|
50
|
+
cat = Rallycat::Cat.new(@api)
|
51
|
+
cat.story(story_num).should == expected
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'parses the defect' do
|
56
|
+
|
57
|
+
expected = <<-STORY
|
58
|
+
|
59
|
+
# [DE1234] - [Rework] Change link to button
|
60
|
+
|
61
|
+
Plan Estimate: 1.0
|
62
|
+
State: In-Progress
|
63
|
+
Task Actual: 0.0
|
64
|
+
Task Estimate: 6.5
|
65
|
+
Task Remaining: 0.5
|
66
|
+
Owner: scootin@fruity.com
|
67
|
+
|
68
|
+
## DESCRIPTION
|
69
|
+
|
70
|
+
This is a defect.
|
71
|
+
|
72
|
+
## TASKS
|
73
|
+
|
74
|
+
STORY
|
75
|
+
|
76
|
+
responder = RallyDefectResponder.new
|
77
|
+
|
78
|
+
Artifice.activate_with responder.endpoint do
|
79
|
+
story_num = 'DE1234'
|
80
|
+
cat = Rallycat::Cat.new(@api)
|
81
|
+
cat.story(story_num).should == expected
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'displays nothing under the tasks section when there are no tasks' do
|
86
|
+
|
87
|
+
|
88
|
+
responder = lambda do |env|
|
89
|
+
[200, {}, [
|
90
|
+
<<-XML
|
91
|
+
<QueryResult>
|
92
|
+
<Results>
|
93
|
+
<Object>
|
94
|
+
<FormattedID>US4567</FormattedID>
|
95
|
+
<Name>Sky Touch</Name>
|
96
|
+
<Description>#{ CGI::escapeHTML('<div>As a user<br /> ISBAT touch the sky.</div>') }</Description>
|
97
|
+
</Object>
|
98
|
+
</Results>
|
99
|
+
<TotalResultCount>1</TotalResultCount>
|
100
|
+
</QueryResult>
|
101
|
+
XML
|
102
|
+
]]
|
103
|
+
end
|
104
|
+
|
105
|
+
expected = <<-STORY
|
106
|
+
|
107
|
+
# [US4567] - Sky Touch
|
108
|
+
|
109
|
+
Plan Estimate:
|
110
|
+
State:
|
111
|
+
Task Actual:
|
112
|
+
Task Estimate:
|
113
|
+
Task Remaining:
|
114
|
+
Owner:
|
115
|
+
|
116
|
+
## DESCRIPTION
|
117
|
+
|
118
|
+
As a user
|
119
|
+
ISBAT touch the sky.
|
120
|
+
|
121
|
+
## TASKS
|
122
|
+
|
123
|
+
STORY
|
124
|
+
|
125
|
+
Artifice.activate_with responder do
|
126
|
+
story_num = 'US4567'
|
127
|
+
cat = Rallycat::Cat.new(@api)
|
128
|
+
cat.story(story_num).should == expected
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rallycat::CLI do
|
4
|
+
it 'should default to STDOUT' do
|
5
|
+
STDOUT.should_receive(:puts).with 'only support for `cat` exists at the moment.'
|
6
|
+
cli = Rallycat::CLI.new []
|
7
|
+
cli.run
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe Rallycat::CLI, '#run' do
|
12
|
+
|
13
|
+
it 'should execute command' do
|
14
|
+
sout = StringIO.new
|
15
|
+
cli = Rallycat::CLI.new ['help'], sout
|
16
|
+
|
17
|
+
cli.run
|
18
|
+
|
19
|
+
sout.rewind
|
20
|
+
sout.read.should == "only support for `cat` exists at the moment.\n"
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
describe Rallycat::Connection do
|
5
|
+
|
6
|
+
before do
|
7
|
+
@responder = lambda do |env|
|
8
|
+
# 'https://rally1.rallydev.com/slm/webservice/current/user'
|
9
|
+
[200, {}, ['<foo>bar</foo>']]
|
10
|
+
end
|
11
|
+
|
12
|
+
@rc_file_path = File.expand_path("~/.rallycatrc")
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#initialize' do
|
16
|
+
|
17
|
+
it "raises when no config file is present" do
|
18
|
+
# Force file to not exist.
|
19
|
+
YAML.stub(:load_file).with(@rc_file_path).and_raise(Errno::ENOENT)
|
20
|
+
|
21
|
+
lambda {
|
22
|
+
Artifice.activate_with @responder do
|
23
|
+
@connection = Rallycat::Connection.new
|
24
|
+
end
|
25
|
+
}.should raise_error Rallycat::InvalidConfigError,
|
26
|
+
'Your rallycat config file is missing or invalid. Please RTFM.'
|
27
|
+
end
|
28
|
+
|
29
|
+
it "raises when the credentials are invalid" do
|
30
|
+
responder = lambda do |env|
|
31
|
+
[401, {}, "Invalid, homes."]
|
32
|
+
end
|
33
|
+
|
34
|
+
YAML.stub(:load_file).with(@rc_file_path).and_return({
|
35
|
+
'username' => "",
|
36
|
+
'password' => ""
|
37
|
+
})
|
38
|
+
|
39
|
+
lambda {
|
40
|
+
Artifice.activate_with responder do
|
41
|
+
@connection = Rallycat::Connection.new
|
42
|
+
end
|
43
|
+
}.should raise_error Rallycat::InvalidCredentialsError,
|
44
|
+
"Your Rally credentials are invalid."
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#api" do
|
49
|
+
|
50
|
+
before do
|
51
|
+
responder = lambda do |env|
|
52
|
+
# 'https://rally1.rallydev.com/slm/webservice/current/user'
|
53
|
+
[200, {}, ['<foo>bar</foo>']]
|
54
|
+
end
|
55
|
+
|
56
|
+
YAML.stub(:load_file).with(@rc_file_path).and_return({
|
57
|
+
'username' => "bitches@rallydev.com",
|
58
|
+
'password' => "wesuckatsoftware"
|
59
|
+
})
|
60
|
+
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
it "creates a rally connection using user's rallycatrc config file" do
|
65
|
+
Artifice.activate_with @responder do
|
66
|
+
@connection = Rallycat::Connection.new
|
67
|
+
end
|
68
|
+
|
69
|
+
rally_connection = @connection.api
|
70
|
+
|
71
|
+
rally_connection.should be_kind_of(RallyRestAPI)
|
72
|
+
|
73
|
+
rally_connection.username.should eq("bitches@rallydev.com")
|
74
|
+
rally_connection.password.should eq("wesuckatsoftware")
|
75
|
+
end
|
76
|
+
|
77
|
+
it "creates a rally connection given the username and password provided" do
|
78
|
+
Artifice.activate_with @responder do
|
79
|
+
@connection = Rallycat::Connection.new("hoes@rallydev.com", "wesuckatpasswords")
|
80
|
+
end
|
81
|
+
|
82
|
+
rally_connection = @connection.api
|
83
|
+
|
84
|
+
rally_connection.should be_kind_of(RallyRestAPI)
|
85
|
+
|
86
|
+
rally_connection.username.should eq("hoes@rallydev.com")
|
87
|
+
rally_connection.password.should eq("wesuckatpasswords")
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe HtmlToTextConverter do
|
4
|
+
|
5
|
+
it 'strips away <p> tags and adds a newline to the end of the paragraph' do
|
6
|
+
html = '<p>This is a one sentence paragraph.</p>'
|
7
|
+
text = HtmlToTextConverter.new(html).parse
|
8
|
+
text.should == "This is a one sentence paragraph.\n"
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns an empty string when given nil' do
|
12
|
+
text = HtmlToTextConverter.new(nil).parse
|
13
|
+
text.should == ''
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'strips away <p> tags and adds a newline to the end of each paragraph' do
|
17
|
+
html = '<p>Paragraph 1</p><p>Paragraph 2</p>'
|
18
|
+
text = HtmlToTextConverter.new(html).parse
|
19
|
+
text.should == "Paragraph 1\nParagraph 2\n"
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'strips away spaces in between tags' do
|
23
|
+
html = '<p>Paragraph 1</p> <p>Paragraph 2</p>'
|
24
|
+
text = HtmlToTextConverter.new(html).parse
|
25
|
+
text.should == "Paragraph 1\nParagraph 2\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'consolidates whitespace' do
|
29
|
+
html = '<p>I like <b>hot sauce</b>.</p>'
|
30
|
+
text = HtmlToTextConverter.new(html).parse
|
31
|
+
text.should == "I like hot sauce.\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'converts nbsp to a regular space' do
|
35
|
+
html = ' hello world '
|
36
|
+
text = HtmlToTextConverter.new(html).parse
|
37
|
+
text.should == " hello world "
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'turns <br /> into \n' do
|
41
|
+
html = '<div>foo<br /> bar</div>'
|
42
|
+
text = HtmlToTextConverter.new(html).parse
|
43
|
+
text.should == "foo\nbar\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'indents and stars items in a list' do
|
47
|
+
html = '<ol><li>Item 1</li><li>Item 2</li><li>Item 3</li></ol>'
|
48
|
+
text = HtmlToTextConverter.new(html).parse
|
49
|
+
text.should == " * Item 1\n * Item 2\n * Item 3\n\n"
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'converts complex HTML to clean plain text' do
|
53
|
+
html = "<div>This is a story:</div> <div><br /></div> <div> <ul> <li>list item 1</li> <li>list item 2</li> </ul> </div> <div>We need to do something important.</div> <div><br /></div> <div>NOTE:</div> <div>- a note with the word <b><font color=\"#ff0000\">red</font></b>:</div> <div>Some more text.</div> <div><br /></div> <div><b>QA:</b></div> <div>Instructions for QA.</div>"
|
54
|
+
|
55
|
+
expected = <<-TEXT
|
56
|
+
This is a story:
|
57
|
+
|
58
|
+
|
59
|
+
* list item 1
|
60
|
+
* list item 2
|
61
|
+
|
62
|
+
|
63
|
+
We need to do something important.
|
64
|
+
|
65
|
+
|
66
|
+
NOTE:
|
67
|
+
- a note with the word red:
|
68
|
+
Some more text.
|
69
|
+
|
70
|
+
|
71
|
+
QA:
|
72
|
+
Instructions for QA.
|
73
|
+
TEXT
|
74
|
+
|
75
|
+
text = HtmlToTextConverter.new(html).parse
|
76
|
+
text.should == expected
|
77
|
+
end
|
78
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper.rb"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
|
8
|
+
require 'rallycat'
|
9
|
+
require 'pry'
|
10
|
+
require 'rally_rest_api'
|
11
|
+
require 'artifice'
|
12
|
+
|
13
|
+
root = File.expand_path('../..', __FILE__)
|
14
|
+
Dir["#{root}/spec/support/**/*.rb"].each { |f| require f }
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
18
|
+
config.run_all_when_everything_filtered = true
|
19
|
+
config.filter_run :focus
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class RallyDefectResponder
|
2
|
+
|
3
|
+
attr_reader :requests
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@requests = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def last_request
|
10
|
+
@requests.last
|
11
|
+
end
|
12
|
+
|
13
|
+
def endpoint
|
14
|
+
lambda do |env|
|
15
|
+
@requests << request = Rack::Request.new(env)
|
16
|
+
|
17
|
+
case request.url
|
18
|
+
when "https://rally1.rallydev.com/slm/webservice/current/Defect?query=%28FormattedId+%3D+DE1234%29&fetch=true"
|
19
|
+
[200, {}, [
|
20
|
+
<<-XML
|
21
|
+
<QueryResult>
|
22
|
+
<Results>
|
23
|
+
<Object>
|
24
|
+
<FormattedID>DE1234</FormattedID>
|
25
|
+
<Name>[Rework] Change link to button</Name>
|
26
|
+
<PlanEstimate>1.0</PlanEstimate>
|
27
|
+
<ScheduleState>In-Progress</ScheduleState>
|
28
|
+
<TaskActualTotal>0.0</TaskActualTotal>
|
29
|
+
<TaskEstimateTotal>6.5</TaskEstimateTotal>
|
30
|
+
<TaskRemainingTotal>0.5</TaskRemainingTotal>
|
31
|
+
<Owner>scootin@fruity.com</Owner>
|
32
|
+
<Description>#{ CGI::escapeHTML('<div><p>This is a defect.</p></div>') }</Description>
|
33
|
+
</Object>
|
34
|
+
</Results>
|
35
|
+
<TotalResultCount>1</TotalResultCount>
|
36
|
+
</QueryResult>
|
37
|
+
XML
|
38
|
+
]]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
class RallyStoryResponder
|
4
|
+
|
5
|
+
attr_reader :requests
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@requests = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def last_request
|
12
|
+
@requests.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def endpoint
|
16
|
+
lambda do |env|
|
17
|
+
@requests << request = Rack::Request.new(env)
|
18
|
+
|
19
|
+
case request.url
|
20
|
+
when 'https://rally1.rallydev.com/slm/webservice/1.17/task/1'
|
21
|
+
[200, {}, [
|
22
|
+
<<-XML
|
23
|
+
<Task refObjectName="Change link to button">
|
24
|
+
<FormattedID>TA1234</FormattedID>
|
25
|
+
<State>Complete</State>
|
26
|
+
<TaskIndex>1</TaskIndex>
|
27
|
+
</Task>
|
28
|
+
XML
|
29
|
+
]]
|
30
|
+
when 'https://rally1.rallydev.com/slm/webservice/1.17/task/2'
|
31
|
+
[200, {}, [
|
32
|
+
<<-XML
|
33
|
+
<Task refObjectName="Add confirmation">
|
34
|
+
<FormattedID>TA1235</FormattedID>
|
35
|
+
<State>In-Progress</State>
|
36
|
+
<TaskIndex>2</TaskIndex>
|
37
|
+
</Task>
|
38
|
+
XML
|
39
|
+
]]
|
40
|
+
when 'https://rally1.rallydev.com/slm/webservice/1.17/task/3'
|
41
|
+
[200, {}, [
|
42
|
+
<<-XML
|
43
|
+
<Task refObjectName="Code Review">
|
44
|
+
<FormattedID>TA1236</FormattedID>
|
45
|
+
<State>Defined</State>
|
46
|
+
<TaskIndex>3</TaskIndex>
|
47
|
+
</Task>
|
48
|
+
XML
|
49
|
+
]]
|
50
|
+
when 'https://rally1.rallydev.com/slm/webservice/1.17/task/4'
|
51
|
+
[200, {}, [
|
52
|
+
<<-XML
|
53
|
+
<Task refObjectName="QA Test">
|
54
|
+
<FormattedID>TA1237</FormattedID>
|
55
|
+
<State>Defined</State>
|
56
|
+
<TaskIndex>4</TaskIndex>
|
57
|
+
</Task>
|
58
|
+
XML
|
59
|
+
]]
|
60
|
+
else
|
61
|
+
# https://rally1.rallydev.com/slm/webservice/current/HierarchicalRequirement?query=%28FormattedId+%3D+US7176%29&fetch=true
|
62
|
+
[200, {}, [
|
63
|
+
<<-XML
|
64
|
+
<QueryResult>
|
65
|
+
<Results>
|
66
|
+
<Object>
|
67
|
+
<FormattedID>US4567</FormattedID>
|
68
|
+
<Name>[Rework] Change link to button</Name>
|
69
|
+
<PlanEstimate>1.0</PlanEstimate>
|
70
|
+
<ScheduleState>In-Progress</ScheduleState>
|
71
|
+
<TaskActualTotal>0.0</TaskActualTotal>
|
72
|
+
<TaskEstimateTotal>6.5</TaskEstimateTotal>
|
73
|
+
<TaskRemainingTotal>0.5</TaskRemainingTotal>
|
74
|
+
<Owner>scootin@fruity.com</Owner>
|
75
|
+
<Description>#{ CGI::escapeHTML('<div><p>This is the story</p></div><ul><li>Remember to do this.</li><li>And this too.</li></ul>') }</Description>
|
76
|
+
<Tasks>
|
77
|
+
<Task ref="https://rally1.rallydev.com/slm/webservice/1.17/task/1" />
|
78
|
+
<Task ref="https://rally1.rallydev.com/slm/webservice/1.17/task/2" />
|
79
|
+
<Task ref="https://rally1.rallydev.com/slm/webservice/1.17/task/3" />
|
80
|
+
<Task ref="https://rally1.rallydev.com/slm/webservice/1.17/task/4" />
|
81
|
+
</Tasks>
|
82
|
+
</Object>
|
83
|
+
</Results>
|
84
|
+
<TotalResultCount>1</TotalResultCount>
|
85
|
+
</QueryResult>
|
86
|
+
XML
|
87
|
+
]]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
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.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,8 +10,41 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2012-
|
14
|
-
dependencies:
|
13
|
+
date: 2012-07-17 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: builder
|
17
|
+
requirement: &70363599472260 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70363599472260
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rally_rest_api
|
28
|
+
requirement: &70363599471700 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70363599471700
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: nokogiri
|
39
|
+
requirement: &70363599471120 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :runtime
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *70363599471120
|
15
48
|
description: The Rally website sucks. CLI is better.
|
16
49
|
email:
|
17
50
|
- adam@adamtanner.org
|
@@ -22,15 +55,29 @@ extensions: []
|
|
22
55
|
extra_rdoc_files: []
|
23
56
|
files:
|
24
57
|
- .gitignore
|
58
|
+
- .rspec
|
25
59
|
- Gemfile
|
60
|
+
- Gemfile.lock
|
26
61
|
- LICENSE
|
27
62
|
- README.md
|
28
63
|
- Rakefile
|
64
|
+
- TODO
|
29
65
|
- bin/rallycat
|
30
66
|
- lib/rallycat.rb
|
31
67
|
- lib/rallycat/cat.rb
|
68
|
+
- lib/rallycat/cli.rb
|
69
|
+
- lib/rallycat/connection.rb
|
70
|
+
- lib/rallycat/html_to_text_converter.rb
|
32
71
|
- lib/rallycat/version.rb
|
33
72
|
- rallycat.gemspec
|
73
|
+
- spec/integration_spec.rb
|
74
|
+
- spec/lib/rallycat/cat_spec.rb
|
75
|
+
- spec/lib/rallycat/cli_spec.rb
|
76
|
+
- spec/lib/rallycat/connection_spec.rb
|
77
|
+
- spec/lib/rallycat/html_to_text_converter_spec.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- spec/support/rally_defect_responder.rb
|
80
|
+
- spec/support/rally_story_responder.rb
|
34
81
|
homepage: https://github.com/adamtanner/rallycat
|
35
82
|
licenses: []
|
36
83
|
post_install_message:
|
@@ -45,7 +92,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
92
|
version: '0'
|
46
93
|
segments:
|
47
94
|
- 0
|
48
|
-
hash: -
|
95
|
+
hash: -2940249692508219253
|
49
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
97
|
none: false
|
51
98
|
requirements:
|
@@ -54,11 +101,19 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
101
|
version: '0'
|
55
102
|
segments:
|
56
103
|
- 0
|
57
|
-
hash: -
|
104
|
+
hash: -2940249692508219253
|
58
105
|
requirements: []
|
59
106
|
rubyforge_project:
|
60
107
|
rubygems_version: 1.8.7
|
61
108
|
signing_key:
|
62
109
|
specification_version: 3
|
63
110
|
summary: The Rally website sucks. CLI is better.
|
64
|
-
test_files:
|
111
|
+
test_files:
|
112
|
+
- spec/integration_spec.rb
|
113
|
+
- spec/lib/rallycat/cat_spec.rb
|
114
|
+
- spec/lib/rallycat/cli_spec.rb
|
115
|
+
- spec/lib/rallycat/connection_spec.rb
|
116
|
+
- spec/lib/rallycat/html_to_text_converter_spec.rb
|
117
|
+
- spec/spec_helper.rb
|
118
|
+
- spec/support/rally_defect_responder.rb
|
119
|
+
- spec/support/rally_story_responder.rb
|