octopolo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +21 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/Gemfile +3 -0
  7. data/Guardfile +5 -0
  8. data/MIT-LICENSE +20 -0
  9. data/README.markdown +55 -0
  10. data/Rakefile +38 -0
  11. data/bash_completion.sh +13 -0
  12. data/bin/octopolo +21 -0
  13. data/bin/op +21 -0
  14. data/lib/octopolo.rb +15 -0
  15. data/lib/octopolo/changelog.rb +27 -0
  16. data/lib/octopolo/cli.rb +210 -0
  17. data/lib/octopolo/commands/accept_pull.rb +8 -0
  18. data/lib/octopolo/commands/compare_release.rb +9 -0
  19. data/lib/octopolo/commands/deployable.rb +8 -0
  20. data/lib/octopolo/commands/github_auth.rb +5 -0
  21. data/lib/octopolo/commands/new_branch.rb +9 -0
  22. data/lib/octopolo/commands/new_deployable.rb +8 -0
  23. data/lib/octopolo/commands/new_staging.rb +8 -0
  24. data/lib/octopolo/commands/octopolo_setup.rb +5 -0
  25. data/lib/octopolo/commands/pivotal_auth.rb +5 -0
  26. data/lib/octopolo/commands/pull_request.rb +13 -0
  27. data/lib/octopolo/commands/signoff.rb +10 -0
  28. data/lib/octopolo/commands/stage_up.rb +8 -0
  29. data/lib/octopolo/commands/stale_branches.rb +11 -0
  30. data/lib/octopolo/commands/sync_branch.rb +11 -0
  31. data/lib/octopolo/commands/tag_release.rb +13 -0
  32. data/lib/octopolo/config.rb +146 -0
  33. data/lib/octopolo/convenience_wrappers.rb +46 -0
  34. data/lib/octopolo/dated_branch_creator.rb +81 -0
  35. data/lib/octopolo/git.rb +262 -0
  36. data/lib/octopolo/github.rb +95 -0
  37. data/lib/octopolo/github/commit.rb +45 -0
  38. data/lib/octopolo/github/pull_request.rb +126 -0
  39. data/lib/octopolo/github/pull_request_creator.rb +127 -0
  40. data/lib/octopolo/github/user.rb +40 -0
  41. data/lib/octopolo/jira/story_commenter.rb +26 -0
  42. data/lib/octopolo/pivotal.rb +44 -0
  43. data/lib/octopolo/pivotal/story_commenter.rb +19 -0
  44. data/lib/octopolo/pull_request_merger.rb +99 -0
  45. data/lib/octopolo/renderer.rb +37 -0
  46. data/lib/octopolo/reports.rb +18 -0
  47. data/lib/octopolo/scripts.rb +23 -0
  48. data/lib/octopolo/scripts/accept_pull.rb +67 -0
  49. data/lib/octopolo/scripts/compare_release.rb +52 -0
  50. data/lib/octopolo/scripts/deployable.rb +27 -0
  51. data/lib/octopolo/scripts/github_auth.rb +87 -0
  52. data/lib/octopolo/scripts/new_branch.rb +34 -0
  53. data/lib/octopolo/scripts/new_deployable.rb +14 -0
  54. data/lib/octopolo/scripts/new_staging.rb +15 -0
  55. data/lib/octopolo/scripts/octopolo_setup.rb +55 -0
  56. data/lib/octopolo/scripts/pivotal_auth.rb +44 -0
  57. data/lib/octopolo/scripts/pull_request.rb +127 -0
  58. data/lib/octopolo/scripts/signoff.rb +85 -0
  59. data/lib/octopolo/scripts/stage_up.rb +26 -0
  60. data/lib/octopolo/scripts/stale_branches.rb +54 -0
  61. data/lib/octopolo/scripts/sync_branch.rb +37 -0
  62. data/lib/octopolo/scripts/tag_release.rb +70 -0
  63. data/lib/octopolo/templates/pull_request_body.erb +24 -0
  64. data/lib/octopolo/user_config.rb +112 -0
  65. data/lib/octopolo/version.rb +3 -0
  66. data/lib/octopolo/week.rb +130 -0
  67. data/octopolo.gemspec +31 -0
  68. data/spec/.DS_Store +0 -0
  69. data/spec/octopolo/cli_spec.rb +310 -0
  70. data/spec/octopolo/config_spec.rb +344 -0
  71. data/spec/octopolo/convenience_wrappers_spec.rb +80 -0
  72. data/spec/octopolo/dated_branch_creator_spec.rb +143 -0
  73. data/spec/octopolo/git_spec.rb +419 -0
  74. data/spec/octopolo/github/commit_spec.rb +59 -0
  75. data/spec/octopolo/github/pull_request_creator_spec.rb +174 -0
  76. data/spec/octopolo/github/pull_request_spec.rb +291 -0
  77. data/spec/octopolo/github/user_spec.rb +65 -0
  78. data/spec/octopolo/github_spec.rb +169 -0
  79. data/spec/octopolo/jira/stor_commenter_spec.rb +30 -0
  80. data/spec/octopolo/pivotal/story_commenter_spec.rb +34 -0
  81. data/spec/octopolo/pivotal_spec.rb +61 -0
  82. data/spec/octopolo/pull_request_merger_spec.rb +144 -0
  83. data/spec/octopolo/renderer_spec.rb +35 -0
  84. data/spec/octopolo/scripts/accept_pull_spec.rb +76 -0
  85. data/spec/octopolo/scripts/compare_release_spec.rb +115 -0
  86. data/spec/octopolo/scripts/deployable_spec.rb +52 -0
  87. data/spec/octopolo/scripts/github_auth_spec.rb +156 -0
  88. data/spec/octopolo/scripts/new_branch_spec.rb +41 -0
  89. data/spec/octopolo/scripts/new_deployable_spec.rb +18 -0
  90. data/spec/octopolo/scripts/new_staging_spec.rb +18 -0
  91. data/spec/octopolo/scripts/octopolo_setup_spec.rb +120 -0
  92. data/spec/octopolo/scripts/pivotal_auth_spec.rb +77 -0
  93. data/spec/octopolo/scripts/pull_request_spec.rb +217 -0
  94. data/spec/octopolo/scripts/signoff_spec.rb +139 -0
  95. data/spec/octopolo/scripts/stage_up_spec.rb +52 -0
  96. data/spec/octopolo/scripts/stale_branches_spec.rb +81 -0
  97. data/spec/octopolo/scripts/sync_branch_spec.rb +57 -0
  98. data/spec/octopolo/scripts/tag_release_spec.rb +108 -0
  99. data/spec/octopolo/user_config_spec.rb +167 -0
  100. data/spec/octopolo_spec.rb +7 -0
  101. data/spec/spec_helper.rb +29 -0
  102. data/spec/support/engine_yard.cache +0 -0
  103. data/spec/support/sample_octopolo.yml +2 -0
  104. data/spec/support/sample_user.yml +2 -0
  105. data/templates/lib.erb +23 -0
  106. data/templates/script.erb +7 -0
  107. data/templates/spec.erb +29 -0
  108. metadata +344 -0
@@ -0,0 +1,37 @@
1
+ require_relative "../scripts"
2
+
3
+ module Octopolo
4
+ module Scripts
5
+ class SyncBranch
6
+ include ConfigWrapper
7
+ include CLIWrapper
8
+ include GitWrapper
9
+
10
+ attr_accessor :branch
11
+
12
+ def self.execute(branch=nil)
13
+ new(branch).execute
14
+ end
15
+
16
+ def initialize(branch=nil)
17
+ @branch = branch || default_branch
18
+ end
19
+
20
+ # Public: Default value of branch if none given
21
+ def default_branch
22
+ config.deploy_branch
23
+ end
24
+
25
+ def execute
26
+ merge_branch
27
+ end
28
+
29
+ # Public: Merge the specified remote branch into your local
30
+ def merge_branch
31
+ git.merge branch
32
+ rescue Git::MergeFailed
33
+ cli.say "Merge failed. Please resolve these conflicts."
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,70 @@
1
+ require "date" # necessary to get the Date.today convenience method
2
+ require_relative "../scripts"
3
+ require_relative "../changelog"
4
+
5
+ module Octopolo
6
+ module Scripts
7
+ class TagRelease
8
+ include CLIWrapper
9
+ include ConfigWrapper
10
+ include GitWrapper
11
+
12
+ attr_accessor :suffix
13
+ attr_accessor :force
14
+ alias_method :force?, :force
15
+
16
+ TIMESTAMP_FORMAT = "%Y.%m.%d.%H.%M"
17
+
18
+ def self.execute(suffix=nil, force=false)
19
+ new(suffix, force).execute
20
+ end
21
+
22
+ def initialize(suffix=nil, force=false)
23
+ @suffix = suffix
24
+ @force = force
25
+ end
26
+
27
+ def execute
28
+ if should_create_branch?
29
+ update_changelog
30
+ tag_release
31
+ else
32
+ raise Octopolo::WrongBranch.new("Must perform this script from the deploy branch (#{config.deploy_branch})")
33
+ end
34
+ end
35
+
36
+ # Public: Whether to create a new branch
37
+ #
38
+ # Returns a Boolean
39
+ def should_create_branch?
40
+ force? || (git.current_branch == config.deploy_branch)
41
+ end
42
+
43
+ # Public: Generate a tag for the current release
44
+ def tag_release
45
+ git.new_tag tag_name
46
+ end
47
+
48
+ # Public: The name to apply to the new tag
49
+ def tag_name
50
+ @tag_name ||= %Q(#{Time.now.strftime(TIMESTAMP_FORMAT)}#{"_#{suffix}" if suffix})
51
+ end
52
+
53
+ def changelog
54
+ @changelog ||= Changelog.new
55
+ end
56
+
57
+ def update_changelog
58
+ changelog.open do |log|
59
+ log.puts "#### #{tag_name}"
60
+ end
61
+ git.perform("add #{changelog.filename}")
62
+ git.perform("commit -m 'Updating Changelog for #{tag_name}'")
63
+ end
64
+
65
+ end
66
+ end
67
+
68
+ WrongBranch = Class.new(StandardError)
69
+ end
70
+
@@ -0,0 +1,24 @@
1
+ <%= description %>
2
+
3
+ Deploy Plan
4
+ -----------
5
+ Does Infrastructure need to know anything special about this deploy? If so, keep this section and fill it in. **Otherwise, delete it.**
6
+
7
+ Rollback Plan
8
+ -------------
9
+ **If this pull request requires anything more complex (e.g., rolling back a migration), you MUST update this section. Otherwise, delete this note.**
10
+
11
+ To roll back this change, revert the merge with `git revert -m 1 MERGE_SHA` and perform another deploy.
12
+
13
+ URLs
14
+ ----
15
+ <% pivotal_ids.each do |pivotal_id| -%>
16
+ * [pivotal tracker story <%= pivotal_id %>](https://www.pivotaltracker.com/story/show/<%= pivotal_id %>)
17
+ <% end -%>
18
+ <% jira_ids.each do |jira_id| -%>
19
+ * [Jira issue <%= jira_id %>](<%= jira_url %>/browse/<%= jira_id %>)
20
+ <% end -%>
21
+
22
+ QA Plan
23
+ -------
24
+ Provide a detailed QA plan, or other developers will retain the right to mock you mercilessly.
@@ -0,0 +1,112 @@
1
+ module Octopolo
2
+ class UserConfig
3
+ # config values
4
+ attr_accessor :github_user
5
+ attr_accessor :github_token
6
+ attr_accessor :full_name
7
+ attr_accessor :pivotal_token
8
+ attr_accessor :attributes # keep the whole hash
9
+
10
+ # Public: Initialize a new UserConfig instance
11
+ def initialize attributes={}
12
+ self.attributes = attributes
13
+ attributes.each do |key, value|
14
+ # e.g., foo: "bar" translates to self.foo = "bar"
15
+ setter = "#{key}="
16
+ send(setter, value) if respond_to? setter
17
+ end
18
+ end
19
+
20
+ # Public: Parse the user's config file
21
+ #
22
+ # Returns a UserConfig instance
23
+ def self.parse
24
+ new attributes_from_file
25
+ end
26
+
27
+ # Public: Set and store a new configuration value
28
+ def set key, value
29
+ # capture new value in instance
30
+ send("#{key}=", value)
31
+ attributes.merge!(key => value)
32
+ # and store it
33
+ File.write UserConfig.config_path, YAML.dump(attributes)
34
+ end
35
+
36
+ # Public: The user's configuration values
37
+ #
38
+ # Returns a Hash
39
+ def self.attributes_from_file
40
+ YAML.load_file config_path
41
+ rescue Errno::ENOENT
42
+ # create the file if it doesn't exist
43
+ touch_config_file
44
+ {}
45
+ end
46
+
47
+ # Public: The path to the users's configuration file
48
+ #
49
+ # Returns a String containing the path
50
+ def self.config_path
51
+ File.join(config_parent, "config.yml")
52
+ end
53
+
54
+ # Public: The parent directory of the user's configuration file
55
+ #
56
+ # Returns a String containing the path
57
+ def self.config_parent
58
+ dir = File.expand_path("~/.octopolo")
59
+ dir = File.expand_path("~/.automation") unless Dir.exists?(dir)
60
+ dir
61
+ end
62
+
63
+ # Public: Create the user's configuration file
64
+ # NOTE this seems a mite gnarly, and its tests worse, but doesn't seem worth splitting out into "create_config_parent_directory" and "create_config_file" at this stage
65
+ def self.touch_config_file
66
+ unless Dir.exist? config_parent
67
+ Dir.mkdir config_parent
68
+ end
69
+
70
+ unless File.exist? config_path
71
+ File.write UserConfig.config_path, YAML.dump({})
72
+ end
73
+ end
74
+
75
+ # Public: The user's name
76
+ #
77
+ # Returns a String
78
+ def full_name
79
+ @full_name || ENV["USER"]
80
+ end
81
+
82
+ # Public: The GitHub username
83
+ #
84
+ # If none is stored, generate it for the user.
85
+ #
86
+ # Returns a String or raises MissingGitHubAuth
87
+ def github_user
88
+ @github_user || raise(MissingGitHubAuth)
89
+ end
90
+
91
+ # Public: The GitHub token
92
+ #
93
+ # If none is stored, generate it for the user.
94
+ #
95
+ # Returns a String or raises MissingGitHubAuth
96
+ def github_token
97
+ @github_token || raise(MissingGitHubAuth)
98
+ end
99
+
100
+ # Public: The Pivotal Tracker token
101
+ #
102
+ # If none is stored, prompt them to generate it.
103
+ #
104
+ # Returns a String or raises MissingPivotalAuth
105
+ def pivotal_token
106
+ @pivotal_token || raise(MissingPivotalAuth)
107
+ end
108
+
109
+ MissingGitHubAuth = Class.new(StandardError)
110
+ MissingPivotalAuth = Class.new(StandardError)
111
+ end
112
+ end
@@ -0,0 +1,3 @@
1
+ module Octopolo
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,130 @@
1
+ module Octopolo
2
+ class Week
3
+ attr_accessor :year
4
+ attr_accessor :number
5
+
6
+ DATE_STRING_FORMAT = "%Y-%m-%d"
7
+
8
+ # Public: Instantiate a new Week
9
+ #
10
+ # year - Year the week occurs in
11
+ # number - The number of the week in the given year
12
+ def initialize year, number
13
+ self.year = year
14
+ self.number = number
15
+ end
16
+
17
+ # Public: Parse the date from a date-ish object
18
+ #
19
+ # dateish - A Date, Time, or String representing a date or time
20
+ def self.parse dateish
21
+ # TODO i'm not 100% happy with this implementation, but it does what i want and the tests pass
22
+ if dateish.respond_to? :to_date
23
+ date = dateish.to_date
24
+ new date.year, date.cweek
25
+ else
26
+ date = Date.parse(dateish.to_s)
27
+ new date.year, date.cweek
28
+ end
29
+ rescue ArgumentError
30
+ raise InvalidType
31
+ end
32
+
33
+ # Public: Instantiate a Week for the current week
34
+ #
35
+ # Returns an instance of Week
36
+ def self.current
37
+ today = Date.today
38
+ Week.new today.cwyear, today.cweek
39
+ end
40
+
41
+ # Public: Instantiate a Week for the prior week
42
+ #
43
+ # Returns an instance of Week
44
+ def self.prior
45
+ current.previous
46
+ end
47
+
48
+ # Public: A list of the last N weeks, excluding the current week
49
+ #
50
+ # count - An integer listing the number of weeks to return
51
+ #
52
+ # Returns an Array of Week instances
53
+ def self.last count
54
+ # holy hell this is gnarly
55
+ #
56
+ # essentially, create an array with the current week in it, then loop
57
+ # through and tack on the previous week for whichever week is last; this
58
+ # way, we keep tacking on ever more previous weeks as many times as is
59
+ # asked for.
60
+ out = [prior]
61
+ (count - 1).times do
62
+ out << out.last.previous
63
+ end
64
+ out
65
+ end
66
+
67
+ # Public: Friendly string representation of the week
68
+ #
69
+ # Returns a String of the start date
70
+ def to_s
71
+ start_date.strftime(DATE_STRING_FORMAT)
72
+ end
73
+
74
+ # Public: Programmer-friendly representation of the week
75
+ #
76
+ # Returns a String containing object details
77
+ def inspect
78
+ "#<#{self.class} #{to_s} (year=#{year} number=#{number})>"
79
+ end
80
+
81
+ # Public: Whether the week is the same as another week
82
+ #
83
+ # Returns a Boolean
84
+ def == other
85
+ year == other.year && number == other.number
86
+ rescue
87
+ false
88
+ end
89
+
90
+ # Public: The previous week
91
+ #
92
+ # Returns an instance of Week
93
+ def previous
94
+ if number == 1
95
+ # carry the 1
96
+ Week.new year - 1, 52
97
+ else
98
+ Week.new year, number - 1
99
+ end
100
+ end
101
+
102
+ # Public: The next week
103
+ #
104
+ # Returns an instance of Week
105
+ def next
106
+ if number == 52
107
+ # carry the 1
108
+ Week.new year + 1, 1
109
+ else
110
+ Week.new year, number + 1
111
+ end
112
+ end
113
+
114
+ # Public: Monday that starts the week
115
+ #
116
+ # Returns an instance of Date
117
+ def start_date
118
+ Date.commercial year, number
119
+ end
120
+
121
+ # Public: Sunday that ends the week
122
+ #
123
+ # Returns an instance of Date
124
+ def end_date
125
+ Date.commercial year, number, 7
126
+ end
127
+
128
+ InvalidType = Class.new(StandardError)
129
+ end
130
+ end
data/octopolo.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/octopolo/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Patrick Byrne", "Luke Ludwig"]
6
+ gem.email = ["patrick.byrne@sportngin.com", "luke.ludwig@sportngin.com"]
7
+ gem.description = %q{A set of Github workflow scripts.}
8
+ gem.summary = %q{A set of Github workflow scripts.}
9
+ gem.homepage = ""
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.name = "octopolo"
15
+ gem.license = "MIT"
16
+ gem.require_paths = ["lib"]
17
+ gem.version = Octopolo::VERSION
18
+
19
+ gem.add_dependency 'rake'
20
+ gem.add_dependency 'gli', '~> 2.10'
21
+ gem.add_dependency 'hashie', '~> 1.2'
22
+ gem.add_dependency 'octokit', '~> 2.7.1'
23
+ gem.add_dependency 'highline', '~> 1.6'
24
+ gem.add_dependency 'pivotal-tracker', '~> 0.5'
25
+ gem.add_dependency 'jiralicious'
26
+
27
+ gem.add_development_dependency 'rspec', '~> 2.11.0'
28
+ gem.add_development_dependency "guard"
29
+ gem.add_development_dependency "guard-rspec"
30
+ gem.add_development_dependency 'octopolo-plugin-example'
31
+ end
data/spec/.DS_Store ADDED
Binary file
@@ -0,0 +1,310 @@
1
+ require "spec_helper"
2
+
3
+ module Octopolo
4
+ describe CLI do
5
+ subject { CLI }
6
+
7
+ context ".perform(command)" do
8
+ let(:command) { "ls" }
9
+ let(:result) { "result" }
10
+ let(:error) { "error message" }
11
+ let(:exception_message) { "Error with something" }
12
+
13
+ it "passes the given command to the shell" do
14
+ subject.should_receive(:say).with(command)
15
+ Open3.should_receive(:capture3).with(command).and_return([result, nil])
16
+ subject.should_receive(:say).with(result)
17
+ subject.perform(command).should == result
18
+ end
19
+
20
+ it "uses Kernel#` if Open3 has no capture3 method (e.g., Ruby 1.8.7)" do
21
+ subject.should_receive(:say).with(command)
22
+ # simulating ruby 1.8.7 not having an Open3.capture3 method
23
+ Open3.should_receive(:respond_to?).with(:capture3).and_return(false)
24
+ subject.should_receive(:`).with(command).and_return(result)
25
+ subject.should_receive(:say).with(result)
26
+ subject.perform(command).should == result
27
+ end
28
+
29
+ it "should handle errors gracefully" do
30
+ subject.should_receive(:say).with(command)
31
+ Open3.should_receive(:capture3).with(command).and_raise(exception_message)
32
+ subject.should_receive(:say).with("Unable to perform '#{command}': #{exception_message}")
33
+ subject.perform(command).should be_nil
34
+ end
35
+
36
+ it "should not speak the command if told not to" do
37
+ subject.should_receive(:say).with(command).never
38
+ subject.perform(command, false)
39
+ end
40
+ end
41
+
42
+ context ".perform_quietly(command)" do
43
+ let(:command) { "ls" }
44
+
45
+ it "performs the command without displaying itself" do
46
+ subject.should_receive(:perform).with(command, false)
47
+ subject.perform_quietly(command)
48
+ end
49
+ end
50
+
51
+ context ".perform_and_exit(command)" do
52
+ let(:command) { "ls" }
53
+
54
+ it "should use the 'exec' command to replace the Ruby process with the command" do
55
+ subject.should_receive(:say).with(command)
56
+ subject.should_receive(:exec).with(command)
57
+ subject.perform_and_exit(command)
58
+ end
59
+ end
60
+
61
+ context ".say(message)" do
62
+ let(:message) { "asdf" }
63
+
64
+ it "displays the given message" do
65
+ subject.should_receive(:puts).with(message)
66
+ subject.say message
67
+ end
68
+
69
+ it "does nothing if the message is nil" do
70
+ subject.should_receive(:puts).never
71
+ subject.say nil
72
+ end
73
+
74
+ it "does nothing if the message is an empty string" do
75
+ subject.should_receive(:puts).never
76
+ subject.say ""
77
+ end
78
+ end
79
+
80
+ context ".spacer_line" do
81
+ it "displays a blank space" do
82
+ subject.should_receive(:say).with(" ")
83
+ subject.spacer_line
84
+ end
85
+ end
86
+
87
+ context ".perform_in(dir, &block)" do
88
+ let(:dir) { "/tmp" }
89
+ let(:command) { "ls" }
90
+
91
+ it "changes the script to the given directory" do
92
+ subject.should_receive(:say).with("Performing in #{dir}:")
93
+ Dir.should_receive(:chdir).with(dir).and_yield
94
+ subject.should_receive(:perform).with(command)
95
+
96
+ subject.perform_in(dir) do
97
+ subject.perform(command)
98
+ end
99
+ end
100
+ end
101
+
102
+ context ".ask(question, choices)" do
103
+ let(:question) { "What do you want to eat?" }
104
+ let(:choices) { ["sandwich", "carrots", "cake"] }
105
+ let(:one_choice) { [choices.first] }
106
+ let(:valid_string_answer) { choices[valid_numeric_answer - 1] }
107
+ let(:invalid_string_answer) { "pineapple" }
108
+ # answers start at 1, not at 0, so +1
109
+ let(:valid_numeric_answer) { rand(choices.size) + 1 }
110
+ let(:invalid_low_numeric_answer) { 0 }
111
+ let(:invalid_high_numeric_answer) { choices.size + 2 }
112
+
113
+ it "provides the given list of choices for the given question" do
114
+ subject.should_receive(:say).with(question)
115
+ subject.should_receive(:say).with("1) sandwich")
116
+ subject.should_receive(:say).with("2) carrots")
117
+ subject.should_receive(:say).with("3) cake")
118
+ subject.should_receive(:prompt).and_return(valid_string_answer) # only specifying return value to prevent infinite loop
119
+
120
+ subject.ask(question, choices)
121
+ end
122
+
123
+ it "skips printing the question and choices if told not to (useful to avoid cluttering spec output)" do
124
+ subject.should_receive(:say).with(question).never
125
+ subject.should_receive(:say).with("1) sandwich").never
126
+ subject.should_receive(:say).with("2) carrots").never
127
+ subject.should_receive(:say).with("3) cake").never
128
+ subject.should_receive(:prompt).and_return(valid_string_answer) # only specifying return value to prevent infinite loop
129
+
130
+ subject.ask(question, choices, true)
131
+ end
132
+
133
+ it "simply returns the value if given only one choice" do
134
+ subject.should_receive(:say).never
135
+ subject.should_receive(:prompt).never
136
+
137
+ subject.ask(question, one_choice).should == one_choice.first
138
+ end
139
+
140
+ context "when answering with the string value" do
141
+ it "returns the user's selection, if in the available choices" do
142
+ subject.should_receive(:prompt).and_return(valid_string_answer)
143
+ subject.ask(question, choices, true).should == valid_string_answer
144
+ end
145
+
146
+ it "asks again if given a string other than one of the choices" do
147
+ subject.should_receive(:prompt).and_return(invalid_string_answer)
148
+ subject.should_receive(:say).with("Not a valid choice.")
149
+ subject.should_receive(:prompt).and_return(valid_string_answer)
150
+
151
+ subject.ask(question, choices, true).should == valid_string_answer
152
+ end
153
+ end
154
+
155
+ context "when answering with the numeric value" do
156
+ it "returns the user's selection, if in the available choices" do
157
+ subject.should_receive(:prompt).and_return(valid_numeric_answer)
158
+ subject.ask(question, choices, true).should == valid_string_answer
159
+ end
160
+
161
+ it "asks again if given a answer 0 or less" do
162
+ subject.should_receive(:prompt).and_return(invalid_low_numeric_answer)
163
+ subject.should_receive(:say).with("Not a valid choice.")
164
+ subject.should_receive(:prompt).and_return(valid_numeric_answer)
165
+
166
+ subject.ask(question, choices, true).should == valid_string_answer
167
+ end
168
+
169
+ it "asks again if given a answer greater than the list of choices" do
170
+ subject.should_receive(:prompt).and_return(invalid_high_numeric_answer)
171
+ subject.should_receive(:say).with("Not a valid choice.")
172
+ subject.should_receive(:prompt).and_return(valid_numeric_answer)
173
+
174
+ subject.ask(question, choices, true).should == valid_string_answer
175
+ end
176
+ end
177
+ end
178
+
179
+ context ".ask_boolean question" do
180
+ let(:question) { "Are you truly happy?" }
181
+
182
+ it "asks the question and prompts for an answer" do
183
+ subject.should_receive(:prompt).with("#{question} (y/n)") { "y" }
184
+ subject.ask_boolean(question)
185
+ end
186
+
187
+ it "returns true for 'y'" do
188
+ subject.should_receive(:prompt) { "y" }
189
+ subject.ask_boolean(question).should be_true
190
+ end
191
+
192
+ it "returns true for 'yes'" do
193
+ subject.should_receive(:prompt) { "yes" }
194
+ subject.ask_boolean(question).should be_true
195
+ end
196
+
197
+ it "returns true for 'Y'" do
198
+ subject.should_receive(:prompt) { "Y" }
199
+ subject.ask_boolean(question).should be_true
200
+ end
201
+
202
+ it "returns false for 'n'" do
203
+ subject.should_receive(:prompt) { "n" }
204
+ subject.ask_boolean(question).should be_false
205
+ end
206
+
207
+ it "returns false for 'no'" do
208
+ subject.should_receive(:prompt) { "no" }
209
+ subject.ask_boolean(question).should be_false
210
+ end
211
+
212
+ it "returns false for 'N'" do
213
+ subject.should_receive(:prompt) { "N" }
214
+ subject.ask_boolean(question).should be_false
215
+ end
216
+ end
217
+
218
+ context ".prompt prompt_text" do
219
+ let(:input) { "asdf" }
220
+ let(:highline) { stub }
221
+ let(:prompt_text) { "Foo: " }
222
+
223
+ before do
224
+ subject.stub(highline: highline)
225
+ end
226
+
227
+ it "retrieves response from the user" do
228
+ highline.should_receive(:ask).with("> ").and_return(input)
229
+
230
+ subject.prompt.should == input
231
+ end
232
+
233
+ it "uses the given text as the prompt" do
234
+ highline.should_receive(:ask).with(prompt_text)
235
+
236
+ subject.prompt prompt_text
237
+ end
238
+ end
239
+
240
+ context ".prompt_secret prompt_text" do
241
+ let(:input) { "asdf" }
242
+ let(:prompt_text) { "Foo: " }
243
+ let(:highline) { stub }
244
+ let(:highline_config) { stub }
245
+
246
+ before do
247
+ subject.stub(highline: highline)
248
+ end
249
+
250
+ it "retrieves response from the user without displaying what they type" do
251
+ highline.should_receive(:ask).with(prompt_text).and_yield(highline_config).and_return(input)
252
+ highline_config.should_receive(:echo=).with(false)
253
+ highline_config.should_receive(:readline=).with(true)
254
+ subject.prompt_secret(prompt_text).should == input
255
+ end
256
+ end
257
+
258
+ context ".prompt_multiline prompt_text" do
259
+ let(:input) { %w(a s d f) }
260
+ let(:prompt_text) { "Steps:" }
261
+ let(:highline) { stub }
262
+ let(:highline_config) { stub }
263
+
264
+ before do
265
+ subject.stub(highline: highline)
266
+ end
267
+
268
+ it "retreives response from the user across multiple lines, returning an array of their input" do
269
+ highline.should_receive(:ask).with(prompt_text).and_yield(highline_config).and_return(input)
270
+ highline_config.should_receive(:gather=).with("")
271
+ highline_config.should_receive(:readline=).with(true)
272
+ subject.prompt_multiline(prompt_text).should == input
273
+ end
274
+ end
275
+
276
+ context ".highline" do
277
+ let(:result) { stub }
278
+
279
+ it "instantiates a HighLine object" do
280
+ HighLine.should_receive(:new) { result }
281
+ subject.highline.should == result
282
+ end
283
+ end
284
+
285
+ context ".open path" do
286
+ let(:path) { "http://example.com/" }
287
+
288
+ it "exits and opens the path with Mac OS X's `open` command" do
289
+ subject.should_receive(:perform_and_exit).with("open '#{path}'")
290
+ subject.open path
291
+ end
292
+ end
293
+
294
+ context ".copy_to_clipboard(input)" do
295
+ let(:input) { "something#{rand(1000)}" }
296
+
297
+ # FIXME figure out a better way to test this on non-Mac systems, which don't have pbcopy or pbpaste
298
+ #
299
+ # only run on the test on a mac and when not in tmux
300
+ if RUBY_PLATFORM.include?("darwin") and !ENV['TMUX']
301
+ it "puts the text on the system clipboard (for pasting)" do
302
+ subject.should_receive(:say).with("Putting '#{input}' on the clipboard.")
303
+ subject.copy_to_clipboard input
304
+ # and to test that it's on the clipboard
305
+ `pbpaste`.should == input
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end