crab 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,11 @@
1
+ module Crab
2
+ class CucumberScenario
3
+ def generate_from(scenario)
4
+ return <<-SCENARIO
5
+
6
+ Scenario: [#{scenario.formatted_id}] #{scenario.name}
7
+ #{scenario.steps.join("\n ")}
8
+ SCENARIO
9
+ end
10
+ end
11
+ end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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