crab 0.1.3 → 0.1.4
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/README.md +26 -6
- data/crab.gemspec +2 -1
- data/features/create-and-delete-story.feature +18 -0
- data/features/find-text-in-stories.feature +11 -10
- data/features/login-and-out-of-rally.feature +1 -1
- data/features/project-selection.feature +6 -8
- data/features/pull-from-rally-into-cucumber.feature +7 -6
- data/features/show-from-rally.feature +17 -11
- data/features/subcommand-help.feature +47 -26
- data/features/update-story-in-rally.feature +3 -3
- data/lib/crab.rb +2 -0
- data/lib/crab/cli.rb +23 -1
- data/lib/crab/create.rb +23 -0
- data/lib/crab/cucumber_feature.rb +9 -2
- data/lib/crab/cucumber_scenario.rb +6 -1
- data/lib/crab/delete.rb +25 -0
- data/lib/crab/pull.rb +4 -4
- data/lib/crab/rally.rb +3 -0
- data/lib/crab/show.rb +1 -1
- data/lib/crab/story.rb +4 -0
- data/lib/crab/version.rb +1 -1
- metadata +27 -16
- data/old/.gitignore +0 -1
- data/old/Gemfile +0 -14
- data/old/Rakefile +0 -277
- data/old/templates/feature-pt.mustache +0 -5
- data/old/templates/feature.mustache +0 -4
data/README.md
CHANGED
@@ -41,7 +41,7 @@ Thankfully, that part is easy:
|
|
41
41
|
Logged in as cv@lixo.org.
|
42
42
|
|
43
43
|
$ crab project "World Domination 3000"
|
44
|
-
$ crab
|
44
|
+
$ crab find
|
45
45
|
US1001: Arms Rockets Upon Successful Boot
|
46
46
|
US1002: Launches Rockets Upon Command from Evil Mastermind
|
47
47
|
US1003: Transfers $0.01 From All Bank Accounts
|
@@ -82,10 +82,31 @@ features to have:
|
|
82
82
|
|
83
83
|
There are more switches. Check out `crab update --help` to find out more.
|
84
84
|
|
85
|
+
i18n Support
|
86
|
+
------------
|
87
|
+
|
88
|
+
`crab` uses [Gherkin][3] internally, so all languages supported by Cucumber are also
|
89
|
+
included:
|
90
|
+
|
91
|
+
$ crab show US1001 -l ja
|
92
|
+
機能: [US1001] Arms Rockets Upon Successful Boot
|
93
|
+
...
|
94
|
+
シナリオ: [TC10388] Rocket Silo Is Unlocked
|
95
|
+
Given a silo where the rockets are stored
|
96
|
+
...
|
97
|
+
|
98
|
+
Unfortunately, we could not think of a decent way to translate the steps themselves
|
99
|
+
(see the `Given` there?), without using Gherkin to parse each step individually and
|
100
|
+
check that it can be used, which seemed a little overkill for now.
|
101
|
+
|
102
|
+
Hopefully this will be enough for your case, but if not please let us know!
|
103
|
+
|
104
|
+
[3]: https://github.com/cucumber/gherkin
|
105
|
+
|
85
106
|
Developing
|
86
107
|
----------
|
87
108
|
|
88
|
-
To develop `crab`, you are going to need [Bundler][
|
109
|
+
To develop `crab`, you are going to need [Bundler][4], [Aruba][5] and a
|
89
110
|
working Rally account with a project set up where you can edit things. The
|
90
111
|
supplied `Gemfile` should take care of everything else:
|
91
112
|
|
@@ -96,8 +117,8 @@ supplied `Gemfile` should take care of everything else:
|
|
96
117
|
|
97
118
|
If you have any problems, please let us know.
|
98
119
|
|
99
|
-
[
|
100
|
-
[
|
120
|
+
[4]: http://gembundler.com
|
121
|
+
[5]: https://github.com/cucumber/aruba
|
101
122
|
|
102
123
|
To do
|
103
124
|
-----
|
@@ -116,9 +137,9 @@ To do
|
|
116
137
|
- Add a `move` subcommand which moves the story from one state to the next (potentially, `move --back`)
|
117
138
|
- Add a Cucumber Formatter that updates Test Runs in Rally with results from CI
|
118
139
|
- Investigate use of other fields like Priority and Risk in Rally Test Cases
|
119
|
-
- Support i18n Cucumber Features
|
120
140
|
- Make it possible to associate defects with Features (essentially treating defects like stories)
|
121
141
|
- Test in Ruby 1.9
|
142
|
+
- Use the Gherkin models and formatters instead of dumb string templates to generate feature files
|
122
143
|
|
123
144
|
Suggestions? Please get in touch!
|
124
145
|
|
@@ -136,7 +157,6 @@ Disclaimers
|
|
136
157
|
|
137
158
|
This project and its authors have no affiliation with Rally Software Development Corp. or the Cucumber project.
|
138
159
|
|
139
|
-
|
140
160
|
It was written as necessity in a real-world project, and by no means should represent endorsement of either product.
|
141
161
|
|
142
162
|
Rally (c) 2003-2011 Rally Software Development Corp.
|
data/crab.gemspec
CHANGED
@@ -19,8 +19,9 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
|
21
21
|
s.add_development_dependency 'aruba'
|
22
|
+
s.add_development_dependency 'cucumber'
|
22
23
|
|
23
|
-
s.add_dependency '
|
24
|
+
s.add_dependency 'gherkin'
|
24
25
|
s.add_dependency 'rally_rest_api'
|
25
26
|
s.add_dependency 'highline'
|
26
27
|
s.add_dependency 'activesupport'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Feature: Create and Delete Stories
|
2
|
+
|
3
|
+
In order to manipulate stories without leaving the terminal
|
4
|
+
A lazy developer
|
5
|
+
Wants to create and delete stories using crab
|
6
|
+
|
7
|
+
Background:
|
8
|
+
Given I am logged in
|
9
|
+
|
10
|
+
Scenario: Create and Delete a Simple Story
|
11
|
+
# When I run `crab create "Dummy Story (created by crab)"`
|
12
|
+
# Then the output should match /US\d{4}: Dummy Story (created by crab) (grooming)/
|
13
|
+
|
14
|
+
# When I run `crab find "Dummy Story"`
|
15
|
+
# Then the output should match /US\d{4}: Dummy Story (created by crab) (grooming)/
|
16
|
+
|
17
|
+
# When I run `crab delete ????`
|
18
|
+
# Then the output should match /Story US\d{4} deleted./
|
@@ -1,21 +1,21 @@
|
|
1
1
|
Feature: Find Text in Stories
|
2
|
-
|
2
|
+
|
3
3
|
In order to find the story ID
|
4
4
|
A lazy developer
|
5
5
|
Wants to search for arbitrary bits of text
|
6
6
|
|
7
|
-
Background:
|
7
|
+
Background:
|
8
8
|
Given I am logged in
|
9
9
|
And I have selected my test project
|
10
10
|
|
11
11
|
Scenario: Matching Name
|
12
12
|
When I run `crab find Sample Crab`
|
13
13
|
Then the output should contain:
|
14
|
-
|
15
|
-
US4988: Sample Crab Story (grooming)
|
16
|
-
US4999: Sample Crab Parent Story (grooming)
|
17
|
-
US5000: Sample Crab Parent Story (grooming)
|
18
|
-
|
14
|
+
"""
|
15
|
+
US4988: Sample Crab Story (grooming)
|
16
|
+
US4999: Sample Crab Parent Story (grooming)
|
17
|
+
US5000: Sample Crab Parent Story (grooming)
|
18
|
+
"""
|
19
19
|
|
20
20
|
@quick
|
21
21
|
Scenario: Project Must be Specified If Not Set
|
@@ -26,6 +26,7 @@ US5000: Sample Crab Parent Story (grooming)
|
|
26
26
|
Scenario: Project Must Exist
|
27
27
|
When I run `crab find --project "foo" pattern`
|
28
28
|
Then the output should contain:
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
"""
|
30
|
+
Error: Project "foo" not found.
|
31
|
+
"""
|
32
|
+
|
@@ -1,27 +1,25 @@
|
|
1
1
|
Feature: Project Selection
|
2
|
-
|
2
|
+
|
3
3
|
In order to work with the find, list etc commands more effectively
|
4
4
|
A lazy developer
|
5
5
|
Wants to set the project persistently across all commands
|
6
6
|
|
7
|
-
Background:
|
7
|
+
Background:
|
8
8
|
Given I am logged in
|
9
9
|
|
10
10
|
Scenario: Selecting a Project
|
11
11
|
Given no project is selected
|
12
|
-
|
13
12
|
When I run `crab project`
|
14
13
|
Then the output should contain "No project currently selected."
|
15
|
-
|
16
14
|
When I select my test project
|
17
15
|
Then the exit status should be 0
|
18
|
-
|
19
16
|
When I run `crab project`
|
20
17
|
Then the output should be the name of my test project
|
21
18
|
|
22
19
|
Scenario: Selecting an Invalid Project
|
23
20
|
When I run `crab project "invalid"`
|
24
21
|
Then the output should contain:
|
25
|
-
|
26
|
-
|
27
|
-
|
22
|
+
"""
|
23
|
+
Error: "invalid" is not a valid project.
|
24
|
+
"""
|
25
|
+
|
@@ -1,10 +1,10 @@
|
|
1
1
|
Feature: Pull From Rally Into Cucumber
|
2
|
-
|
2
|
+
|
3
3
|
In order to begin development of a story that was written in Rally
|
4
4
|
A developer who doesn't want to open a browser or click things
|
5
5
|
Wants the story converted into the much nicer Cucumber format
|
6
6
|
|
7
|
-
Background:
|
7
|
+
Background:
|
8
8
|
Given I am logged in
|
9
9
|
|
10
10
|
Scenario: Pulling a Single Story
|
@@ -15,11 +15,12 @@ Feature: Pull From Rally Into Cucumber
|
|
15
15
|
And a directory named "features" should exist
|
16
16
|
And a file named "features/grooming/US4988-sample-crab-story.feature" should exist
|
17
17
|
And the file "features/grooming/US4988-sample-crab-story.feature" should contain exactly:
|
18
|
-
|
19
|
-
|
18
|
+
"""
|
19
|
+
# language: en
|
20
|
+
Feature: [US4988] Sample Crab Story
|
20
21
|
|
21
|
-
Sample Description
|
22
|
-
|
22
|
+
Sample Description
|
23
|
+
"""
|
23
24
|
|
24
25
|
Scenario: Pulling Multiple Stories
|
25
26
|
When I run `crab pull US4988 US5000`
|
@@ -1,26 +1,32 @@
|
|
1
1
|
Feature: Show Story From Rally
|
2
|
-
|
2
|
+
|
3
3
|
In order to see what's in a story
|
4
4
|
A lazy developer
|
5
5
|
Wants to be able to do it from the command line
|
6
6
|
|
7
|
-
Background:
|
7
|
+
Background:
|
8
8
|
Given I am logged in
|
9
9
|
|
10
10
|
Scenario: Show Simple Story
|
11
11
|
When I run `crab show US4988`
|
12
12
|
Then the output should contain:
|
13
|
-
|
14
|
-
Feature: [US4988] Sample Crab Story
|
13
|
+
"""
|
14
|
+
Feature: [US4988] Sample Crab Story
|
15
15
|
|
16
|
-
Sample Description
|
17
|
-
|
16
|
+
Sample Description
|
17
|
+
"""
|
18
18
|
|
19
19
|
Scenario: Show Story With Test Cases
|
20
20
|
When I run `crab show US5000`
|
21
21
|
Then the output should contain "Feature: [US5000] Sample Crab Parent Story"
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
22
|
+
And the output should contain "@manual @functional"
|
23
|
+
And the output should contain "Scenario: [TC10388] Sample Testcase"
|
24
|
+
And the output should contain " Given Rally behaves"
|
25
|
+
And the output should contain " When I look at the test case steps"
|
26
|
+
And the output should contain " Then I should be able to export them into Cucumber format"
|
27
|
+
|
28
|
+
Scenario: Story In Different Language
|
29
|
+
When I run `crab show US5000 --language pt`
|
30
|
+
Then the output should contain "Funcionalidade: "
|
31
|
+
And the output should contain "Cenario: "
|
32
|
+
|
@@ -1,6 +1,6 @@
|
|
1
1
|
@quick
|
2
2
|
Feature: Subcommand Help
|
3
|
-
|
3
|
+
|
4
4
|
In order to learn how to use crab
|
5
5
|
A newbie developer
|
6
6
|
Wants to see a useful help message when she runs crab with the wrong arguments
|
@@ -11,46 +11,66 @@ Feature: Subcommand Help
|
|
11
11
|
|
12
12
|
Scenario: Help
|
13
13
|
When I run `crab -h`
|
14
|
-
Then the output should contain "
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
14
|
+
Then the output should contain " create Create a new story in Rally"
|
15
|
+
Then the output should contain " delete Delete an existing story in Rally"
|
16
|
+
And the output should contain " find Find stories by text in name, description or notes"
|
17
|
+
And the output should contain " login Persistently authenticate user with Rally"
|
18
|
+
And the output should contain " project Persistently select project to work with in Rally"
|
19
|
+
And the output should contain " pull Downloads stories (and its test cases) as Cucumber feature files"
|
20
|
+
And the output should contain " show Show a story (and its test cases) as a Cucumber feature"
|
21
|
+
And the output should contain " update Update a story (name, estimate, etc)"
|
20
22
|
|
21
23
|
Scenario: Bogus Subcommand
|
22
24
|
When I run `crab bogus`
|
23
25
|
Then the output should contain:
|
24
|
-
|
25
|
-
Error: Unknown subcommand "bogus".
|
26
|
-
|
26
|
+
"""
|
27
|
+
Error: Unknown subcommand "bogus".
|
28
|
+
"""
|
27
29
|
|
28
30
|
Scenario: Pull Subcommand
|
29
31
|
When I run `crab pull --help`
|
30
32
|
Then the output should contain:
|
31
|
-
|
32
|
-
crab pull: pulls stories from Rally and writes them out as Cucumber features
|
33
|
+
"""
|
34
|
+
crab pull: pulls stories from Rally and writes them out as Cucumber features
|
33
35
|
|
34
|
-
Usage: crab [options] pull story1 [story2 ...]
|
35
|
-
|
36
|
+
Usage: crab [options] pull story1 [story2 ...]
|
37
|
+
"""
|
36
38
|
|
37
39
|
Scenario: Show Subcommand
|
38
40
|
When I run `crab show --help`
|
39
41
|
Then the output should contain:
|
40
|
-
|
41
|
-
crab show: displays a story in Rally as a Cucumber feature
|
42
|
+
"""
|
43
|
+
crab show: displays a story in Rally as a Cucumber feature
|
44
|
+
|
45
|
+
Usage: crab [options] show story
|
46
|
+
"""
|
47
|
+
|
48
|
+
Scenario: Create Subcommand
|
49
|
+
When I run `crab create --help`
|
50
|
+
Then the output should contain:
|
51
|
+
"""
|
52
|
+
crab create: create a new story in Rally
|
42
53
|
|
43
|
-
Usage: crab [options]
|
44
|
-
|
54
|
+
Usage: crab [options] create name [options]
|
55
|
+
"""
|
56
|
+
|
57
|
+
Scenario: Delete Subcommand
|
58
|
+
When I run `crab delete --help`
|
59
|
+
Then the output should contain:
|
60
|
+
"""
|
61
|
+
crab delete: delete an existing story in Rally
|
62
|
+
|
63
|
+
Usage: crab [options] delete story [options]
|
64
|
+
"""
|
45
65
|
|
46
66
|
Scenario: Update Subcommand
|
47
67
|
When I run `crab update --help`
|
48
68
|
Then the output should contain:
|
49
|
-
|
50
|
-
crab update: update a story in Rally
|
69
|
+
"""
|
70
|
+
crab update: update a story in Rally
|
51
71
|
|
52
|
-
Usage: crab [options] update story [options]
|
53
|
-
|
72
|
+
Usage: crab [options] update story [options]
|
73
|
+
"""
|
54
74
|
|
55
75
|
Scenario: Update Needs a Story Number
|
56
76
|
When I run `crab update`
|
@@ -63,8 +83,9 @@ Usage: crab [options] update story [options]
|
|
63
83
|
Scenario: Find Subcommand
|
64
84
|
When I run `crab find --help`
|
65
85
|
Then the output should contain:
|
66
|
-
|
67
|
-
crab find: find a story in Rally
|
86
|
+
"""
|
87
|
+
crab find: find a story in Rally
|
88
|
+
|
89
|
+
Usage: crab [options] find [options] [text]
|
90
|
+
"""
|
68
91
|
|
69
|
-
Usage: crab [options] find [options] [text]
|
70
|
-
"""
|
@@ -1,10 +1,10 @@
|
|
1
1
|
Feature: Update Story in Rally
|
2
|
-
|
2
|
+
|
3
3
|
In order to change a story's fields in Rally
|
4
4
|
A developer who doesn't want to open a browser or click on things
|
5
5
|
Wants to update those fields from the command line
|
6
6
|
|
7
|
-
Background:
|
7
|
+
Background:
|
8
8
|
Given I am logged in
|
9
9
|
|
10
10
|
Scenario: Update Name
|
@@ -15,7 +15,6 @@ Feature: Update Story in Rally
|
|
15
15
|
When I run `crab update US4988 --blocked`
|
16
16
|
Then the output should contain "US4988: Sample Crab Story (grooming)"
|
17
17
|
And the story US4988 should be blocked
|
18
|
-
|
19
18
|
When I run `crab update US4988 --unblocked`
|
20
19
|
Then the output should contain "US4988: Sample Crab Story (grooming)"
|
21
20
|
And the story US4988 should be unblocked
|
@@ -31,3 +30,4 @@ Feature: Update Story in Rally
|
|
31
30
|
Scenario: Setting Parent
|
32
31
|
When I run `crab update US4988 --parent US5000`
|
33
32
|
Then the story US4988 should have US5000 as its parent
|
33
|
+
|
data/lib/crab.rb
CHANGED
data/lib/crab/cli.rb
CHANGED
@@ -2,7 +2,7 @@ require 'trollop'
|
|
2
2
|
|
3
3
|
module Crab
|
4
4
|
|
5
|
-
SUB_COMMANDS = %w(pull login list update show find project)
|
5
|
+
SUB_COMMANDS = %w(pull login list update show find project create delete)
|
6
6
|
|
7
7
|
class CLI
|
8
8
|
def self.start
|
@@ -11,6 +11,8 @@ module Crab
|
|
11
11
|
banner """
|
12
12
|
crab version #{Crab::VERSION}: A Cucumber-Rally bridge
|
13
13
|
|
14
|
+
create Create a new story in Rally
|
15
|
+
delete Delete an existing story in Rally
|
14
16
|
find Find stories by text in name, description or notes
|
15
17
|
login Persistently authenticate user with Rally
|
16
18
|
project Persistently select project to work with in Rally
|
@@ -28,6 +30,7 @@ crab version #{Crab::VERSION}: A Cucumber-Rally bridge
|
|
28
30
|
banner "crab pull: pulls stories from Rally and writes them out as Cucumber features
|
29
31
|
|
30
32
|
Usage: crab [options] pull story1 [story2 ...]"
|
33
|
+
opt :language, "Language to generate Cucumber features in (ISO code)", :default => "en", :short => "-l"
|
31
34
|
end
|
32
35
|
|
33
36
|
Crab::Pull.new(global_opts, cmd_opts, ARGV).run
|
@@ -37,6 +40,7 @@ Usage: crab [options] pull story1 [story2 ...]"
|
|
37
40
|
banner "crab show: displays a story in Rally as a Cucumber feature
|
38
41
|
|
39
42
|
Usage: crab [options] show story"
|
43
|
+
opt :language, "Language to display Cucumber features in (ISO code)", :default => "en", :short => "-l"
|
40
44
|
end
|
41
45
|
|
42
46
|
Crab::Show.new(global_opts, cmd_opts, ARGV).run
|
@@ -88,6 +92,24 @@ Usage: crab [options] project name"
|
|
88
92
|
|
89
93
|
Crab::Project.new(global_opts, cmd_opts, ARGV).run
|
90
94
|
|
95
|
+
when "create"
|
96
|
+
cmd_opts = Trollop::options do
|
97
|
+
banner "crab create: create a new story in Rally
|
98
|
+
|
99
|
+
Usage: crab [options] create name [options]"
|
100
|
+
end
|
101
|
+
|
102
|
+
Crab::Create.new(global_opts, cmd_opts, ARGV).run
|
103
|
+
|
104
|
+
when "delete"
|
105
|
+
cmd_opts = Trollop::options do
|
106
|
+
banner "crab delete: delete an existing story in Rally
|
107
|
+
|
108
|
+
Usage: crab [options] delete story [options]"
|
109
|
+
end
|
110
|
+
|
111
|
+
Crab::Delete.new(global_opts, cmd_opts, ARGV).run
|
112
|
+
|
91
113
|
else
|
92
114
|
if cmd
|
93
115
|
Trollop::die "Unknown subcommand #{cmd.inspect}"
|
data/lib/crab/create.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Crab
|
2
|
+
|
3
|
+
class Create
|
4
|
+
|
5
|
+
def initialize(global_opts, cmd_opts, args)
|
6
|
+
@global_opts = global_opts
|
7
|
+
@cmd_opts = cmd_opts
|
8
|
+
@args = args
|
9
|
+
@rally = Rally.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
name = @args.join(" ")
|
14
|
+
Trollop::die "Please specify a name for the story" if name.blank?
|
15
|
+
|
16
|
+
@rally.connect
|
17
|
+
story = @rally.create_story :name => name
|
18
|
+
|
19
|
+
puts "#{story.formatted_id}: #{story.name} (#{story.state})"
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -1,15 +1,22 @@
|
|
1
1
|
require 'fileutils'
|
2
|
+
require 'gherkin/i18n'
|
2
3
|
|
3
4
|
module Crab
|
4
5
|
|
5
6
|
class CucumberFeature
|
7
|
+
|
8
|
+
def initialize(language)
|
9
|
+
@language = Gherkin::I18n.new(language)
|
10
|
+
end
|
11
|
+
|
6
12
|
def generate_from(story)
|
7
13
|
text = <<-FEATURE
|
8
|
-
|
14
|
+
# language: #{@language.iso_code}
|
15
|
+
#{@language.keywords('feature').last}: [#{story.formatted_id}] #{story.name}
|
9
16
|
|
10
17
|
#{story.description}
|
11
18
|
|
12
|
-
#{Array(story.scenarios).map {|scenario| CucumberScenario.new.generate_from scenario }}
|
19
|
+
#{Array(story.scenarios).map {|scenario| CucumberScenario.new(@language.iso_code).generate_from scenario }}
|
13
20
|
FEATURE
|
14
21
|
text.strip
|
15
22
|
end
|
@@ -1,11 +1,16 @@
|
|
1
1
|
module Crab
|
2
2
|
class CucumberScenario
|
3
|
+
|
4
|
+
def initialize(language)
|
5
|
+
@language = Gherkin::I18n.new(language)
|
6
|
+
end
|
7
|
+
|
3
8
|
def generate_from(scenario)
|
4
9
|
tags = [scenario.method, scenario.test_type]
|
5
10
|
return <<-SCENARIO
|
6
11
|
|
7
12
|
#{tags.map {|t| " @" + t.strip }.join}
|
8
|
-
|
13
|
+
#{@language.keywords('scenario').last}: [#{scenario.formatted_id}] #{scenario.name}
|
9
14
|
#{scenario.steps.join("\n ")}
|
10
15
|
SCENARIO
|
11
16
|
end
|
data/lib/crab/delete.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Crab
|
2
|
+
|
3
|
+
class Delete
|
4
|
+
|
5
|
+
def initialize(global_opts, cmd_opts, args)
|
6
|
+
@global_opts = global_opts
|
7
|
+
@cmd_opts = cmd_opts
|
8
|
+
@args = args
|
9
|
+
@rally = Rally.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
story_id = @args.join(" ")
|
14
|
+
Trollop::die "Story ID must be specified" if story_id.blank?
|
15
|
+
|
16
|
+
@rally.connect
|
17
|
+
story = @rally.find_story_with_id story_id
|
18
|
+
|
19
|
+
story.delete
|
20
|
+
|
21
|
+
puts "Story #{story_id} deleted."
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
data/lib/crab/pull.rb
CHANGED
@@ -4,9 +4,9 @@ module Crab
|
|
4
4
|
|
5
5
|
class Pull
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
9
|
-
@
|
7
|
+
def initialize(global_opts, cmd_opts, story_numbers)
|
8
|
+
@global_opts = global_opts
|
9
|
+
@cmd_opts = cmd_opts
|
10
10
|
@story_numbers = story_numbers
|
11
11
|
@rally = Crab::Rally.new
|
12
12
|
end
|
@@ -24,7 +24,7 @@ module Crab
|
|
24
24
|
::FileUtils.touch story.full_file_name
|
25
25
|
|
26
26
|
File.open(story.full_file_name, "w") do |file|
|
27
|
-
file.write CucumberFeature.new.generate_from story
|
27
|
+
file.write CucumberFeature.new(@cmd_opts[:language]).generate_from story
|
28
28
|
end
|
29
29
|
end
|
30
30
|
end
|
data/lib/crab/rally.rb
CHANGED
data/lib/crab/show.rb
CHANGED
data/lib/crab/story.rb
CHANGED
data/lib/crab/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: crab
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 4
|
10
|
+
version: 0.1.4
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Carlos Villela
|
@@ -44,10 +44,10 @@ dependencies:
|
|
44
44
|
segments:
|
45
45
|
- 0
|
46
46
|
version: "0"
|
47
|
-
type: :
|
47
|
+
type: :development
|
48
48
|
version_requirements: *id002
|
49
49
|
- !ruby/object:Gem::Dependency
|
50
|
-
name:
|
50
|
+
name: gherkin
|
51
51
|
prerelease: false
|
52
52
|
requirement: &id003 !ruby/object:Gem::Requirement
|
53
53
|
none: false
|
@@ -61,7 +61,7 @@ dependencies:
|
|
61
61
|
type: :runtime
|
62
62
|
version_requirements: *id003
|
63
63
|
- !ruby/object:Gem::Dependency
|
64
|
-
name:
|
64
|
+
name: rally_rest_api
|
65
65
|
prerelease: false
|
66
66
|
requirement: &id004 !ruby/object:Gem::Requirement
|
67
67
|
none: false
|
@@ -75,7 +75,7 @@ dependencies:
|
|
75
75
|
type: :runtime
|
76
76
|
version_requirements: *id004
|
77
77
|
- !ruby/object:Gem::Dependency
|
78
|
-
name:
|
78
|
+
name: highline
|
79
79
|
prerelease: false
|
80
80
|
requirement: &id005 !ruby/object:Gem::Requirement
|
81
81
|
none: false
|
@@ -89,7 +89,7 @@ dependencies:
|
|
89
89
|
type: :runtime
|
90
90
|
version_requirements: *id005
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
|
-
name:
|
92
|
+
name: activesupport
|
93
93
|
prerelease: false
|
94
94
|
requirement: &id006 !ruby/object:Gem::Requirement
|
95
95
|
none: false
|
@@ -103,7 +103,7 @@ dependencies:
|
|
103
103
|
type: :runtime
|
104
104
|
version_requirements: *id006
|
105
105
|
- !ruby/object:Gem::Dependency
|
106
|
-
name:
|
106
|
+
name: i18n
|
107
107
|
prerelease: false
|
108
108
|
requirement: &id007 !ruby/object:Gem::Requirement
|
109
109
|
none: false
|
@@ -117,7 +117,7 @@ dependencies:
|
|
117
117
|
type: :runtime
|
118
118
|
version_requirements: *id007
|
119
119
|
- !ruby/object:Gem::Dependency
|
120
|
-
name:
|
120
|
+
name: sanitize
|
121
121
|
prerelease: false
|
122
122
|
requirement: &id008 !ruby/object:Gem::Requirement
|
123
123
|
none: false
|
@@ -130,6 +130,20 @@ dependencies:
|
|
130
130
|
version: "0"
|
131
131
|
type: :runtime
|
132
132
|
version_requirements: *id008
|
133
|
+
- !ruby/object:Gem::Dependency
|
134
|
+
name: trollop
|
135
|
+
prerelease: false
|
136
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
hash: 3
|
142
|
+
segments:
|
143
|
+
- 0
|
144
|
+
version: "0"
|
145
|
+
type: :runtime
|
146
|
+
version_requirements: *id009
|
133
147
|
description: CRaB is a bridge between Cucumber and Rally
|
134
148
|
email:
|
135
149
|
- cvillela@thoughtworks.com
|
@@ -147,6 +161,7 @@ files:
|
|
147
161
|
- Rakefile
|
148
162
|
- bin/crab
|
149
163
|
- crab.gemspec
|
164
|
+
- features/create-and-delete-story.feature
|
150
165
|
- features/find-text-in-stories.feature
|
151
166
|
- features/login-and-out-of-rally.feature
|
152
167
|
- features/project-selection.feature
|
@@ -158,8 +173,10 @@ files:
|
|
158
173
|
- features/update-story-in-rally.feature
|
159
174
|
- lib/crab.rb
|
160
175
|
- lib/crab/cli.rb
|
176
|
+
- lib/crab/create.rb
|
161
177
|
- lib/crab/cucumber_feature.rb
|
162
178
|
- lib/crab/cucumber_scenario.rb
|
179
|
+
- lib/crab/delete.rb
|
163
180
|
- lib/crab/find.rb
|
164
181
|
- lib/crab/login.rb
|
165
182
|
- lib/crab/project.rb
|
@@ -171,12 +188,6 @@ files:
|
|
171
188
|
- lib/crab/update.rb
|
172
189
|
- lib/crab/utilities.rb
|
173
190
|
- lib/crab/version.rb
|
174
|
-
- old/.gitignore
|
175
|
-
- old/Gemfile
|
176
|
-
- old/Gemfile.lock
|
177
|
-
- old/Rakefile
|
178
|
-
- old/templates/feature-pt.mustache
|
179
|
-
- old/templates/feature.mustache
|
180
191
|
has_rdoc: true
|
181
192
|
homepage: http://github.com/cv/crab
|
182
193
|
licenses: []
|
data/old/.gitignore
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
./crab/Gemfile.lock
|
data/old/Gemfile
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
source "http://rubygems.org"
|
2
|
-
|
3
|
-
gem 'rally_rest_api'
|
4
|
-
gem 'cucumber'
|
5
|
-
gem 'rake'
|
6
|
-
|
7
|
-
gem 'highline', :require => 'highline/import' # password prompts, etc
|
8
|
-
gem 'pry' # irb-like awesomeness for debugging
|
9
|
-
gem 'activesupport', :require => 'active_support/all' # nice additions to Ruby API
|
10
|
-
gem 'i18n'
|
11
|
-
|
12
|
-
gem 'mustache' # for file templates
|
13
|
-
gem 'sanitize' # clean up description HTML
|
14
|
-
|
data/old/Rakefile
DELETED
@@ -1,277 +0,0 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'bundler/setup'
|
3
|
-
|
4
|
-
Bundler.require :default
|
5
|
-
|
6
|
-
task :ensure_credentials_are_present do
|
7
|
-
ENV['RALLY_USERNAME'] ||= ask('Username: ')
|
8
|
-
ENV['RALLY_PASSWORD'] ||= ask('Password: ') {|q| q.echo = "*" }
|
9
|
-
end
|
10
|
-
|
11
|
-
task :rally => :ensure_credentials_are_present do
|
12
|
-
@rally = RallyRestAPI.new :username => ENV['RALLY_USERNAME'], :password => ENV['RALLY_PASSWORD']
|
13
|
-
puts "Logged in as #{@rally.user.login_name}"
|
14
|
-
end
|
15
|
-
|
16
|
-
# a few things in this script break / do the wrong thing if you don't have
|
17
|
-
# the project set up or don't have access to it
|
18
|
-
task :project => :rally do
|
19
|
-
project = ENV['RALLY_PROJECT'] ||= ask('Project: ')
|
20
|
-
@project = @rally.find(:project) { equal :name, ENV['RALLY_PROJECT']}.first
|
21
|
-
puts "Using project \"#{ENV['RALLY_PROJECT']}\""
|
22
|
-
end
|
23
|
-
|
24
|
-
task :default => :rally do
|
25
|
-
binding.pry
|
26
|
-
end
|
27
|
-
|
28
|
-
class Feature < Mustache
|
29
|
-
|
30
|
-
self.template_path = File.join(File.dirname(__FILE__), 'templates')
|
31
|
-
|
32
|
-
# didn't like what's being done to support multiple languages
|
33
|
-
# this is going to break horribly when we try to add supoort for generating the
|
34
|
-
# scenarios from Rally and the whole roundtripping thing
|
35
|
-
# (as it'll be difficult to use partials, for example)
|
36
|
-
def initialize(rally_story, language=(ENV['CUCUMBER_LANG'] || ''))
|
37
|
-
@delegate = rally_story
|
38
|
-
self.class.template_file = File.join(self.class.template_path, "feature-#{language}.mustache") if language.present?
|
39
|
-
end
|
40
|
-
|
41
|
-
def name
|
42
|
-
@delegate.name
|
43
|
-
end
|
44
|
-
|
45
|
-
def formatted_id
|
46
|
-
@delegate.formatted_i_d
|
47
|
-
end
|
48
|
-
|
49
|
-
def state
|
50
|
-
(@delegate.schedule_state || "unknown").parameterize.underscore
|
51
|
-
end
|
52
|
-
|
53
|
-
def description
|
54
|
-
# this could use a lot of rethinking :(
|
55
|
-
# biggest problem is that Cucumber breaks if text in description looks like something
|
56
|
-
# it might know how to parse, but doesn't
|
57
|
-
# our feature descriptions are quite like that, so I was being ultra-conservative
|
58
|
-
sanitize(@delegate.description || '').gsub(/ +/, "\n").gsub(/\n\n/, "\n").gsub(/\n/, "\n ")
|
59
|
-
end
|
60
|
-
|
61
|
-
def file_name
|
62
|
-
"#{formatted_id}_#{name.parameterize.underscore}.feature"
|
63
|
-
end
|
64
|
-
|
65
|
-
def dir_name
|
66
|
-
state
|
67
|
-
end
|
68
|
-
|
69
|
-
end
|
70
|
-
|
71
|
-
task :generate_features => :project do
|
72
|
-
project = @project
|
73
|
-
@stories = @rally.find(:hierarchical_requirement, :fetch => true) { equal :project, project }
|
74
|
-
|
75
|
-
@stories.each do |story|
|
76
|
-
feature = Feature.new(story)
|
77
|
-
|
78
|
-
dir = File.join("features", feature.dir_name)
|
79
|
-
file = File.join(dir, feature.file_name)
|
80
|
-
|
81
|
-
FileUtils.mkdir_p dir
|
82
|
-
FileUtils.touch file
|
83
|
-
|
84
|
-
File.open(file, 'w') do |f|
|
85
|
-
f.write feature.render
|
86
|
-
end
|
87
|
-
putc '.'
|
88
|
-
end
|
89
|
-
puts
|
90
|
-
|
91
|
-
end
|
92
|
-
# this whole class should die and we should probably use
|
93
|
-
# a Cucumber formatter instead of relying directly on the Gherkin
|
94
|
-
# APIs. With the extra info that Cucumber provides, it should also
|
95
|
-
# be possible to generate Test Case Runs, which would be pretty sweet
|
96
|
-
# to track using charts in Rally -- eg "How often do I have to do a manual
|
97
|
-
# test run of the @manual stuff so it's all good?"
|
98
|
-
class FeatureProxy
|
99
|
-
def initialize
|
100
|
-
@scenarios = []
|
101
|
-
@steps = ActiveSupport::OrderedHash.new([])
|
102
|
-
end
|
103
|
-
|
104
|
-
attr_reader :scenarios, :steps
|
105
|
-
|
106
|
-
def name
|
107
|
-
@feature.name
|
108
|
-
end
|
109
|
-
|
110
|
-
def description
|
111
|
-
@feature.description
|
112
|
-
end
|
113
|
-
|
114
|
-
# the next two methods could be replaced with something like an extension to the cucumber syntax
|
115
|
-
# in which we annotate foreign IDs -- my proposal is something like what's in here:
|
116
|
-
#
|
117
|
-
# Feature: [XXXXX] Lorem Ipsum
|
118
|
-
#
|
119
|
-
# Simple regex, fairly hard to get wrong in almost any western keyboard, and should work for any bug/issue/card/story tracker I've seen
|
120
|
-
#
|
121
|
-
def rally_id
|
122
|
-
self.name.match(/^\[([^\]]+)\](.*)$/)
|
123
|
-
$1
|
124
|
-
end
|
125
|
-
|
126
|
-
def title_for_rally
|
127
|
-
self.name.match(/^\[([^\]]+)\](.*)$/)
|
128
|
-
$2.squish.titleize
|
129
|
-
end
|
130
|
-
|
131
|
-
# needed by Gherkin
|
132
|
-
def uri(uri)
|
133
|
-
end
|
134
|
-
|
135
|
-
def feature(feature)
|
136
|
-
@feature = feature
|
137
|
-
end
|
138
|
-
|
139
|
-
def get
|
140
|
-
@feature
|
141
|
-
end
|
142
|
-
|
143
|
-
def scenario(scenario)
|
144
|
-
@scenarios << scenario
|
145
|
-
end
|
146
|
-
|
147
|
-
def step(step)
|
148
|
-
@steps[@scenarios.last] += Array(step)
|
149
|
-
end
|
150
|
-
|
151
|
-
def eof
|
152
|
-
end
|
153
|
-
|
154
|
-
def has_story_id?
|
155
|
-
!!self.name.match(/^\[([^\]]+)\](.*)$/)
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def parse(feature)
|
160
|
-
updater = FeatureProxy.new
|
161
|
-
parser = Gherkin::Parser::Parser.new(updater, false, "root", false)
|
162
|
-
parser.parse File.read(feature), feature, 0
|
163
|
-
updater
|
164
|
-
end
|
165
|
-
|
166
|
-
# took a while to figure out that we need to remove the CSS from inside embedded <style> tags!
|
167
|
-
# Rally uses some crazy rich text editor that I'd be soooooo happy to disable, somehow. Chrome Extension, perhaps?
|
168
|
-
def sanitize(source)
|
169
|
-
Sanitize.clean source, :remove_contents => %w{style}
|
170
|
-
end
|
171
|
-
|
172
|
-
task :update_features => :rally do
|
173
|
-
Dir['features/**/*.feature'].sort {|a,b| File.mtime(b) <=> File.mtime(a) }.each do |file|
|
174
|
-
feature = parse file
|
175
|
-
unless feature.has_story_id?
|
176
|
-
raise "Incompatible feature name: #{feature.name} in #{file} (needs to begin with a story number in square brackets)"
|
177
|
-
end
|
178
|
-
|
179
|
-
# TODO inline variables
|
180
|
-
feature_id = feature.rally_id
|
181
|
-
feature_name = feature.title_for_rally
|
182
|
-
|
183
|
-
story = @rally.find(:hierarchical_requirement) { equal :formatted_i_d, feature_id }.first
|
184
|
-
|
185
|
-
updates = {} # collect all updates first (room for double-checking and you only get to call Rally over the net once per item)
|
186
|
-
|
187
|
-
if story.name != feature_name
|
188
|
-
updates[:name] = feature_name
|
189
|
-
end
|
190
|
-
|
191
|
-
# matching the descriptions sucked -- Rally does some conversion to strip out HTML
|
192
|
-
rally_description = sanitize(story.description || '').gsub(/\s+/, ' ').strip
|
193
|
-
cuke_description = sanitize(feature.description || '').gsub(/\s+/, ' ').strip
|
194
|
-
|
195
|
-
if rally_description != cuke_description
|
196
|
-
formatted_description = (feature.description || '').strip.gsub(/\n/, '<br/>')
|
197
|
-
updates[:description] = formatted_description
|
198
|
-
end
|
199
|
-
#
|
200
|
-
|
201
|
-
if updates.empty?
|
202
|
-
puts "Nothing to do for #{feature_id} (story already up to date)"
|
203
|
-
else
|
204
|
-
story.update updates
|
205
|
-
puts "Updated #{feature_id}: #{story.name} (#{updates.keys.join(',')})"
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
task :update_scenarios => :project do
|
211
|
-
Dir['features/**/*.feature'].sort {|a,b| File.mtime(b) <=> File.mtime(a) }.each do |file|
|
212
|
-
feature = parse file
|
213
|
-
feature.steps.each do |scenario, steps|
|
214
|
-
create_test_case @project, feature, scenario, steps
|
215
|
-
|
216
|
-
# TODO make the scenarios in the Cucumber file reflect what's in Rally as well
|
217
|
-
#
|
218
|
-
# goal is to have roundtrip editing, but if not whatever is in Cucumber is "the truth"
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
TYPE_TAGS = %w{acceptance functional non-functional performance regression usability user_interface}
|
224
|
-
RISK_TAGS = %w{low_risk medium_risk high_risk}
|
225
|
-
PRIORITY_TAGS = %{useful important critical}
|
226
|
-
METHOD_TAGS = %{automated manual}
|
227
|
-
|
228
|
-
def create_test_case(project, feature, scenario, steps)
|
229
|
-
if !scenario.name.match /^\[([^\]]+)\](.*)$/
|
230
|
-
# uh oh, we have to create this scenario!
|
231
|
-
tags = scenario.tags.map {|t| t.name.gsub(/^@/, '') }
|
232
|
-
|
233
|
-
# could definitely use some cleaning up, but the defaults seem sensible so far
|
234
|
-
type_tag = (tags.find {|t| TYPE_TAGS.include? t } || 'acceptance').humanize
|
235
|
-
risk_tag = (tags.find {|t| RISK_TAGS.include? t } || 'medium_risk').gsub(/_risk/, '').humanize
|
236
|
-
priority_tag = (tags.find {|t| PRIORITY_TAGS.include? t } || 'important').humanize
|
237
|
-
method_tag = (tags.find {|t| METHOD_TAGS.include? t } || 'automated').humanize
|
238
|
-
|
239
|
-
story = @rally.find(:hierarchical_requirement) { equal :formatted_i_d, feature.rally_id }.first
|
240
|
-
|
241
|
-
options = {
|
242
|
-
:name => scenario.name,
|
243
|
-
:description => "Automatically updated by Cucumber, do not edit",
|
244
|
-
:type => type_tag,
|
245
|
-
:risk => risk_tag,
|
246
|
-
:priority => priority_tag,
|
247
|
-
:method => method_tag,
|
248
|
-
:pre_conditions => "N/A", # do scenarios have a header? BeforeScenario?
|
249
|
-
:post_conditions => "N/A", # ...and/or a footer? AfterScenario, perhaps?
|
250
|
-
:work_product => story,
|
251
|
-
:project => @project # linking testcase to story is not enough
|
252
|
-
}
|
253
|
-
|
254
|
-
test_case = @rally.create(:test_case, options) do |test_case|
|
255
|
-
steps.each_with_index do |step, i|
|
256
|
-
test_case_step = @rally.create(:test_case_step, :test_case => test_case, :index => i, :input => "#{step.keyword.strip} #{step.name.strip}")
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
puts "Created test case #{test_case.formatted_i_d} in story #{feature.rally_id}"
|
261
|
-
else
|
262
|
-
# TODO update scenario text in feature with ID from Rally
|
263
|
-
# then update other fields in Rally with our scenario fields / tags
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
desc "DANGER DANGER DANGER"
|
268
|
-
task :delete_all_test_cases => :project do
|
269
|
-
story_id = ENV['STORY'] ||= ask("Story #: ")
|
270
|
-
project = @project
|
271
|
-
story = @rally.find(:hierarchical_requirement) { equal :formatted_i_d, story_id }.first
|
272
|
-
test_cases = @rally.find(:test_case) { equal :work_product, story }
|
273
|
-
|
274
|
-
test_cases.each do |tc|
|
275
|
-
p tc.delete
|
276
|
-
end
|
277
|
-
end
|