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