crab 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Rakefile +9 -0
- data/bin/crab +5 -0
- data/crab.gemspec +33 -0
- data/features/find-text-in-stories.feature +31 -0
- data/features/list-from-rally.feature +27 -0
- data/features/login-and-out-of-rally.feature +14 -0
- data/features/project-selection.feature +27 -0
- data/features/pull-from-rally-into-cucumber.feature +28 -0
- data/features/show-from-rally.feature +25 -0
- data/features/steps/rally_steps.rb +94 -0
- data/features/subcommand-help.feature +84 -0
- data/features/support/aruba.rb +14 -0
- data/features/update-story-in-rally.feature +33 -0
- data/lib/crab.rb +19 -0
- data/lib/crab/cli.rb +112 -0
- data/lib/crab/cucumber_feature.rb +17 -0
- data/lib/crab/cucumber_scenario.rb +11 -0
- data/lib/crab/find.rb +30 -0
- data/lib/crab/list.rb +29 -0
- data/lib/crab/login.rb +25 -0
- data/lib/crab/project.rb +39 -0
- data/lib/crab/pull.rb +32 -0
- data/lib/crab/rally.rb +50 -0
- data/lib/crab/scenario.rb +23 -0
- data/lib/crab/show.rb +17 -0
- data/lib/crab/story.rb +63 -0
- data/lib/crab/update.rb +68 -0
- data/lib/crab/utilities.rb +18 -0
- data/lib/crab/version.rb +3 -0
- data/old/.gitignore +1 -0
- data/old/Gemfile +14 -0
- data/old/Rakefile +277 -0
- data/old/templates/feature-pt.mustache +5 -0
- data/old/templates/feature.mustache +4 -0
- metadata +230 -0
data/lib/crab.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "crab/version"
|
2
|
+
|
3
|
+
require "crab/utilities"
|
4
|
+
require "crab/rally"
|
5
|
+
require "crab/story"
|
6
|
+
require "crab/cli"
|
7
|
+
require "crab/pull"
|
8
|
+
require "crab/login"
|
9
|
+
require "crab/list"
|
10
|
+
require "crab/find"
|
11
|
+
require "crab/update"
|
12
|
+
require "crab/show"
|
13
|
+
require "crab/scenario"
|
14
|
+
require "crab/project"
|
15
|
+
require "crab/cucumber_feature"
|
16
|
+
require "crab/cucumber_scenario"
|
17
|
+
|
18
|
+
module Crab
|
19
|
+
end
|
data/lib/crab/cli.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
|
3
|
+
module Crab
|
4
|
+
|
5
|
+
SUB_COMMANDS = %w(pull login list update show find project)
|
6
|
+
|
7
|
+
class CLI
|
8
|
+
def self.start
|
9
|
+
global_opts = Trollop::options do
|
10
|
+
version "crab version #{Crab::VERSION}"
|
11
|
+
banner """
|
12
|
+
crab version #{Crab::VERSION}: A Cucumber-Rally bridge
|
13
|
+
|
14
|
+
login Persistently authenticate user with Rally
|
15
|
+
project Persistently select project to work with in Rally
|
16
|
+
list Lists stories
|
17
|
+
update Update a story (name, estimate, etc)
|
18
|
+
show Show a story (and its test cases) as a Cucumber feature
|
19
|
+
pull Downloads stories (and its test cases) as Cucumber feature files
|
20
|
+
find Find stories by text in name, description or notes
|
21
|
+
"""
|
22
|
+
stop_on SUB_COMMANDS
|
23
|
+
end
|
24
|
+
|
25
|
+
cmd = ARGV.shift # get the subcommand
|
26
|
+
case cmd
|
27
|
+
when "pull" # parse delete options
|
28
|
+
cmd_opts = Trollop::options do
|
29
|
+
banner "crab pull: pulls stories from Rally and writes them out as Cucumber features
|
30
|
+
|
31
|
+
Usage: crab [options] pull story1 [story2 ...]"
|
32
|
+
end
|
33
|
+
|
34
|
+
Crab::Pull.new(global_opts, cmd_opts, ARGV).run
|
35
|
+
|
36
|
+
when "show"
|
37
|
+
cmd_opts = Trollop::options do
|
38
|
+
banner "crab show: displays a story in Rally as a Cucumber feature
|
39
|
+
|
40
|
+
Usage: crab [options] show story"
|
41
|
+
end
|
42
|
+
|
43
|
+
Crab::Show.new(global_opts, cmd_opts, ARGV).run
|
44
|
+
|
45
|
+
when "login"
|
46
|
+
cmd_opts = Trollop::options do
|
47
|
+
banner "crab login: logs into Rally
|
48
|
+
|
49
|
+
Usage: crab [options] login"
|
50
|
+
opt :username, "Username", :type => String, :short => "-u"
|
51
|
+
opt :password, "Password", :type => String, :short => "-p"
|
52
|
+
end
|
53
|
+
|
54
|
+
Crab::Login.new(global_opts, cmd_opts).run
|
55
|
+
|
56
|
+
when "list"
|
57
|
+
cmd_opts = Trollop::options do
|
58
|
+
banner "crab list: lists stories in Rally
|
59
|
+
|
60
|
+
Usage: crab [options] list"
|
61
|
+
opt :pagesize, "Number of items to fetch per page", :short => "-s", :default => 100
|
62
|
+
opt :project, "Project to use (required unless set by 'crab project')", :short => "-p", :type => String
|
63
|
+
end
|
64
|
+
|
65
|
+
Crab::List.new(global_opts, cmd_opts).run
|
66
|
+
|
67
|
+
when "update"
|
68
|
+
cmd_opts = Trollop::options do
|
69
|
+
banner "crab update: update a story in Rally
|
70
|
+
|
71
|
+
Usage: crab [options] update story [options]"
|
72
|
+
opt :name, "Name (title)", :type => String, :short => "-n"
|
73
|
+
opt :state, "State (one of: #{Crab::Story::VALID_STATES.join(" ")})", :type => String, :short => "-t"
|
74
|
+
opt :estimate, "Estimate", :type => :int, :short => "-e"
|
75
|
+
opt :iteration, "Iteration", :type => String, :short => "-i"
|
76
|
+
opt :release, "Release", :type => String, :short => "-r"
|
77
|
+
opt :blocked, "Blocked", :short => "-b"
|
78
|
+
opt :unblocked, "Unblocked", :short => "-u"
|
79
|
+
opt :parent, "Parent", :type => String, :short => "-p"
|
80
|
+
end
|
81
|
+
|
82
|
+
Crab::Update.new(global_opts, cmd_opts, ARGV).run
|
83
|
+
|
84
|
+
when "find"
|
85
|
+
cmd_opts = Trollop::options do
|
86
|
+
banner "crab find: find a story in Rally
|
87
|
+
|
88
|
+
Usage: crab [options] find [options] text"
|
89
|
+
opt :project, "Project to use (required unless set by 'crab project')", :short => "-p", :type => String
|
90
|
+
end
|
91
|
+
|
92
|
+
Crab::Find.new(global_opts, cmd_opts, ARGV).run
|
93
|
+
|
94
|
+
when "project"
|
95
|
+
cmd_opts = Trollop::options do
|
96
|
+
banner "crab project: persistently select project to work with in Rally
|
97
|
+
|
98
|
+
Usage: crab [options] project name"
|
99
|
+
end
|
100
|
+
|
101
|
+
Crab::Project.new(global_opts, cmd_opts, ARGV).run
|
102
|
+
|
103
|
+
else
|
104
|
+
if cmd
|
105
|
+
Trollop::die "Unknown subcommand #{cmd.inspect}"
|
106
|
+
else
|
107
|
+
Trollop::die "Unknown subcommand"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Crab
|
4
|
+
|
5
|
+
class CucumberFeature
|
6
|
+
def generate_from(story)
|
7
|
+
text = <<-FEATURE
|
8
|
+
Feature: [#{story.formatted_id}] #{story.name}
|
9
|
+
|
10
|
+
#{story.description}
|
11
|
+
|
12
|
+
#{Array(story.scenarios).map {|scenario| CucumberScenario.new.generate_from scenario }}
|
13
|
+
FEATURE
|
14
|
+
text.strip
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/crab/find.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Crab
|
2
|
+
class Find
|
3
|
+
|
4
|
+
include Utilities
|
5
|
+
|
6
|
+
def initialize(global_opts, cmd_opts, args)
|
7
|
+
@global_opts = global_opts
|
8
|
+
@cmd_opts = cmd_opts
|
9
|
+
@args = args
|
10
|
+
@rally = Rally.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
pattern = @args.map(&:strip).reject(&:empty?)
|
15
|
+
Trollop::die "No search pattern given" if pattern.empty?
|
16
|
+
|
17
|
+
project_name = valid_project_name(@cmd_opts)
|
18
|
+
|
19
|
+
@rally.connect
|
20
|
+
|
21
|
+
project = @rally.find_project(project_name)
|
22
|
+
|
23
|
+
Trollop::die "Project #{@cmd_opts[:project].inspect} not found" if project.nil?
|
24
|
+
|
25
|
+
@rally.find_story(project, pattern).each do |story|
|
26
|
+
puts "#{story.formatted_id}: #{story.name} (#{story.state})"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/crab/list.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Crab
|
2
|
+
class List
|
3
|
+
|
4
|
+
include Utilities
|
5
|
+
|
6
|
+
def initialize(global_opts, cmd_opts)
|
7
|
+
@global_opts = global_opts
|
8
|
+
@cmd_opts = cmd_opts
|
9
|
+
@rally = Rally.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
@rally.connect
|
14
|
+
|
15
|
+
project_name = valid_project_name(@cmd_opts)
|
16
|
+
|
17
|
+
opts = {
|
18
|
+
:pagesize => @cmd_opts[:pagesize],
|
19
|
+
:project => @rally.find_project(project_name),
|
20
|
+
}
|
21
|
+
|
22
|
+
Trollop::die "Project #{@cmd_opts[:project].inspect} not found" if opts[:project].nil?
|
23
|
+
|
24
|
+
@rally.find_all_stories(opts).each do |story|
|
25
|
+
puts "#{story.formatted_id}: #{story.name} (#{story.state})"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/crab/login.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
|
3
|
+
module Crab
|
4
|
+
class Login
|
5
|
+
|
6
|
+
include Utilities
|
7
|
+
|
8
|
+
def initialize(global_opts, cmd_opts)
|
9
|
+
@global_opts = global_opts
|
10
|
+
@cmd_opts = cmd_opts
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
username = @cmd_opts[:username_given] ? @cmd_opts[:username] : ask("Username: ")
|
15
|
+
password = @cmd_opts[:password_given] ? @cmd_opts[:password] : ask("Password: ") {|q| q.echo = false }
|
16
|
+
|
17
|
+
File.open(credentials_file, 'w') do |file|
|
18
|
+
file.puts username
|
19
|
+
file.puts password
|
20
|
+
end
|
21
|
+
|
22
|
+
puts "Logged in as #{username}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/crab/project.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Crab
|
2
|
+
class Project
|
3
|
+
|
4
|
+
def self.current_project_name
|
5
|
+
if File.exists? ".rally_project"
|
6
|
+
File.read(".rally_project").strip
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(global_opts, cmd_opts, args)
|
11
|
+
@global_opts = global_opts
|
12
|
+
@cmd_opts = cmd_opts
|
13
|
+
@args = args
|
14
|
+
@rally = Rally.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
current_project_name = self.class.current_project_name
|
19
|
+
if current_project_name.present?
|
20
|
+
puts current_project_name
|
21
|
+
|
22
|
+
elsif @args.reject {|arg| arg.blank? }.empty?
|
23
|
+
puts "No project currently selected."
|
24
|
+
|
25
|
+
else
|
26
|
+
name = @args.join(" ").strip
|
27
|
+
|
28
|
+
@rally.connect
|
29
|
+
|
30
|
+
project = @rally.find_project name
|
31
|
+
Trollop::die "#{name.inspect} is not a valid project" if project.nil?
|
32
|
+
|
33
|
+
File.open(".rally_project", "w") do |file|
|
34
|
+
file.puts project.name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/crab/pull.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
|
3
|
+
module Crab
|
4
|
+
|
5
|
+
class Pull
|
6
|
+
|
7
|
+
def initialize(global_options, pull_options, story_numbers)
|
8
|
+
@global_options = global_options
|
9
|
+
@pull_options = pull_options
|
10
|
+
@story_numbers = story_numbers
|
11
|
+
@rally = Crab::Rally.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
@rally.connect
|
16
|
+
|
17
|
+
@story_numbers.each do |story_number|
|
18
|
+
story = @rally.find_story_with_id story_number
|
19
|
+
Trollop::die "Could not find story with ID #{story_number}" if story.nil?
|
20
|
+
|
21
|
+
puts "#{story.formatted_id}: #{story.full_file_name}"
|
22
|
+
|
23
|
+
::FileUtils.mkdir_p File.dirname(story.full_file_name)
|
24
|
+
::FileUtils.touch story.full_file_name
|
25
|
+
|
26
|
+
File.open(story.full_file_name, "w") do |file|
|
27
|
+
file.write CucumberFeature.new.generate_from story
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/crab/rally.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rally_rest_api'
|
2
|
+
|
3
|
+
module Crab
|
4
|
+
class Rally
|
5
|
+
|
6
|
+
include Utilities
|
7
|
+
|
8
|
+
def connect
|
9
|
+
get_credentials
|
10
|
+
@rally = ::RallyRestAPI.new :username => @username, :password => @password
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_credentials
|
14
|
+
@username, @password = File.read(valid_credentials_file).split /\n/
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_story_with_id story_id
|
18
|
+
Crab::Story.new @rally.find(:hierarchical_requirement) { equal :formatted_i_d, story_id }.first
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_all_stories(opts={})
|
22
|
+
@rally.find_all(:hierarchical_requirement, {:fetch => true}.merge(opts)).map {|s| Crab::Story.new s }
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_story(project, pattern)
|
26
|
+
@rally.find(:hierarchical_requirement, :fetch => true, :project => project) {
|
27
|
+
pattern.each do |word|
|
28
|
+
_or_ {
|
29
|
+
contains :name, word
|
30
|
+
contains :description, word
|
31
|
+
contains :notes, word
|
32
|
+
}
|
33
|
+
end
|
34
|
+
}.map {|s| Crab::Story.new s }
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_project(name)
|
38
|
+
@rally.find(:project, :fetch => true) { equal :name, name }.first
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_iteration_by_name(name)
|
42
|
+
@rally.find(:iteration) { equal :name, name }.first
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_release_by_name(name)
|
46
|
+
@rally.find(:release) { equal :name, name }.first
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Crab
|
2
|
+
|
3
|
+
class Scenario
|
4
|
+
|
5
|
+
def initialize(rally_test_case)
|
6
|
+
@rally_test_case = rally_test_case
|
7
|
+
end
|
8
|
+
|
9
|
+
def formatted_id
|
10
|
+
@rally_test_case.formatted_i_d
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
@rally_test_case.name
|
15
|
+
end
|
16
|
+
|
17
|
+
def steps
|
18
|
+
Array(@rally_test_case.steps).map {|step| step.input }
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
data/lib/crab/show.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Crab
|
2
|
+
class Show
|
3
|
+
def initialize(global_opts, cmd_opts, args)
|
4
|
+
@global_opts = global_opts
|
5
|
+
@cmd_opts = cmd_opts
|
6
|
+
@story_id = args.first
|
7
|
+
@rally = Rally.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
@rally.connect
|
12
|
+
|
13
|
+
story = @rally.find_story_with_id @story_id
|
14
|
+
puts CucumberFeature.new.generate_from story
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/crab/story.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'sanitize'
|
2
|
+
|
3
|
+
module Crab
|
4
|
+
|
5
|
+
class Story
|
6
|
+
|
7
|
+
VALID_STATES = %w{Grooming Defined In-Progress Completed Accepted Released}
|
8
|
+
|
9
|
+
def initialize(rally_story)
|
10
|
+
@rally_story = rally_story
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
@rally_story.name
|
15
|
+
end
|
16
|
+
|
17
|
+
def file_name
|
18
|
+
"#{formatted_id}-#{name.parameterize.dasherize}.feature"
|
19
|
+
end
|
20
|
+
|
21
|
+
def full_file_name
|
22
|
+
"features/#{state}/#{file_name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def formatted_id
|
26
|
+
@rally_story.formatted_i_d
|
27
|
+
end
|
28
|
+
|
29
|
+
def state
|
30
|
+
(@rally_story.schedule_state || "unknown").parameterize.underscore
|
31
|
+
end
|
32
|
+
|
33
|
+
def description
|
34
|
+
# this could use a lot of rethinking :(
|
35
|
+
# biggest problem is that Cucumber breaks if text in description looks like something
|
36
|
+
# it might know how to parse, but doesn't
|
37
|
+
# our feature descriptions are quite like that, so I was being ultra-conservative
|
38
|
+
sanitize(@rally_story.description || '').gsub(/ +/, "\n").gsub(/\n\n/, "\n").gsub(/\n/, "\n ")
|
39
|
+
end
|
40
|
+
|
41
|
+
def scenarios
|
42
|
+
Array(@rally_story.test_cases).map {|tc| Scenario.new tc }
|
43
|
+
end
|
44
|
+
|
45
|
+
def update(opts)
|
46
|
+
@rally_story.update opts
|
47
|
+
end
|
48
|
+
|
49
|
+
def rally_object
|
50
|
+
@rally_story
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# took a while to figure out that we need to remove the CSS from inside embedded <style> tags!
|
56
|
+
# Rally uses some crazy rich text editor that I'd be soooooo happy to disable, somehow. Chrome Extension, perhaps?
|
57
|
+
def sanitize(source)
|
58
|
+
Sanitize.clean source, :remove_contents => %w{style}
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|