rainforest-cli 0.0.17 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 17f0506c347475feb787f15df4c3db6abd53bae7
4
- data.tar.gz: 27985a231afb31f370324ecc8992b9d2cc9714ef
3
+ metadata.gz: ba717beff0c92befa7902d611e480080e717c607
4
+ data.tar.gz: 377bbc58b3ea971c1a028db37480245dfa808dc7
5
5
  SHA512:
6
- metadata.gz: 50867b13b94b3d1b335a1997c81596d4083c4d12f1275599b1af56514b0677eeff65e11b830d2fe9e18d3397ad48c5393b51dd512e4b347059558666cd49c553
7
- data.tar.gz: 29182d051069e39381c7388ae53b8952aa0ff93280430b59456a3e4d82e108058acba1bf88c4f7c180e895ebadfc08838a1170b34b54a774545d2fccb11499ab
6
+ metadata.gz: daa1731fd6dab4c06c3d6f5ed07a81f7104b299498476eda2b370913e86dac4ebaf4dabc799305dfd647af5a5792949154bef81e8692a68f5a640bb3e4715606
7
+ data.tar.gz: 3bd19eb1863e67789b83aa6bf7597c61cf0a7d01cf442a7cc149ffb3fdad84b819e10246a4f51b64587392f549fa6e3c12fc381040f0ca7bdfa26c013b514bf8
data/README.md CHANGED
@@ -4,42 +4,67 @@
4
4
 
5
5
  A command line interface to interact with RainforestQA.
6
6
 
7
+ This is the easiest way to integrate Rainforest with your deploy scripts or CI server. See [our documentation](http://support.rainforestqa.com/hc/en-us/sections/200597986-Continuous-Integration) on the subject.
8
+
7
9
  ## Installation
8
10
 
9
- $ gem install rainforest-cli
11
+ You can install rainforest-cli with the [gem](https://rubygems.org/) utility.
12
+
13
+ ```bash
14
+ gem install rainforest-cli
15
+ ```
16
+
17
+ Alternatively, you can add to your Gemfile if you're in a ruby project.
18
+
19
+ ```ruby
20
+ gem "rainforest-cli", require: false
21
+ ```
10
22
 
11
23
  ## Basic Usage
12
24
  To use the cli client, you'll need your API token from a test settings page from inside [Rainforest](https://app.rainforestqa.com/).
13
25
 
14
26
  Run all of your tests
15
27
 
16
- rainforest run all --token YOUR_TOKEN_HERE
28
+ ```bash
29
+ rainforest run all --token YOUR_TOKEN_HERE
30
+ ```
17
31
 
18
32
  Run all in the foreground and report
19
33
 
20
- rainforest run all --fg --token YOUR_TOKEN_HERE
34
+ ```bash
35
+ rainforest run all --fg --token YOUR_TOKEN_HERE
36
+ ```
21
37
 
22
38
  Run all tests with tag 'run-me' and abort previous in-progress runs.
23
39
 
24
- rainforest run --tag run-me --fg --conflict abort --token YOUR_TOKEN_HERE
40
+ ```bash
41
+ rainforest run --tag run-me --fg --conflict abort --token YOUR_TOKEN_HERE
42
+ ```
25
43
 
26
44
 
27
45
  ## Options
28
46
 
47
+ ### General
48
+
29
49
  Required:
30
50
  - `--token <your-rainforest-token>` - you must supply your token (get it from any tests API tab)
31
51
 
52
+
53
+ ### Running Tests
32
54
  The options are:
33
55
 
34
56
  - `--browsers ie8` or `--browsers ie8,chrome` - specficy the browsers you wish to run against. This overrides the test own settings. Valid browsers are ie8, ie9, chrome, firefox and safari.
35
57
  - `--tag run-me` - only run tests which have this tag (recommended if you have lots of [test-steps](http://docs.rainforestqa.com/pages/example-test-suite.html#test_steps))!)
36
- - `--conflict abort` - if you trigger rainforest more than once, anything running will be aborted and a fresh run started
37
- - `--fail-fast` - fail the build as soon as the first failed result comes in. If you don't pass this it will wait until 100% of the run is done
38
- - `--fg` - results in the foreground - this is what you want to make the build pass / fail dependent on rainforest results
39
58
  - `--site-id` - only run tests for a specific site. Get in touch with us for help on getting that you site id if you are unable to.
40
- - `--custom-url` - use a custom url for this run. Example use case: an ad-hoc QA environment with [Fourchette](https://github.com/rainforestapp/fourchette). You will need to specify a `site_id` too for this to work. Please note that we will be creating environments under the hood and will not affect your test permanently.
59
+ - `--conflict abort` - if you trigger rainforest more than once, anything running will be aborted and a fresh run started
60
+ - `--fg` - results in the foreground - rainforest-cli will not return until the run is complete. This is what you want to make the build pass / fail dependent on rainforest results
61
+ - `--fail-fast` - fail the build as soon as the first failed result comes in. If you don't pass this it will wait until 100% of the run is done. Use with `--fg`.
62
+ - `--custom-url` - use a custom url for this run. Example use case: an ad-hoc QA environment with [Fourchette](https://github.com/rainforestapp/fourchette). You will need to specify a `site_id` too for this to work. Note that we will be creating a new environment for this particular run.
41
63
  - `--git-trigger` - only trigger a run when the last commit (for a git repo in the current working directory) has contains `@rainforest` and a list of one or more tags. E.g. "Fix checkout process. @rainforest #checkout" would trigger a run for everything tagged `checkout`. This over-rides `--tag` and any tests specified. If no `@rainforest` is detected it will exit 0.
42
64
 
65
+ ## Support
66
+
67
+ Email [help@rainforestqa.com](mailto:help@rainforestqa.com) if you're having trouble using this gem or need help to integrate Rainforest in your CI or deployment flow.
43
68
 
44
69
  ## Contributing
45
70
 
data/Rakefile CHANGED
@@ -1 +1,11 @@
1
1
  require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
9
+ rescue LoadError
10
+ end
11
+
@@ -1,5 +1,7 @@
1
1
  require "rainforest/cli/version"
2
2
  require "rainforest/cli/options"
3
+ require "rainforest/cli/runner"
4
+ require "rainforest/cli/http_client"
3
5
  require "rainforest/cli/git_trigger"
4
6
  require "rainforest/cli/csv_importer"
5
7
  require "httparty"
@@ -8,176 +10,29 @@ require "logger"
8
10
 
9
11
  module Rainforest
10
12
  module Cli
11
- API_URL = 'https://app.rainforestqa.com/api/1'.freeze
12
-
13
13
  def self.start(args)
14
- @options = OptionParser.new(args)
14
+ options = OptionParser.new(args)
15
15
  OptionParser.new(['--help']) if args.size == 0
16
16
 
17
- unless @options.token
18
- logger.fatal "You must pass your API token using: --token TOKEN"
19
- exit 2
20
- end
21
-
22
- if @options.custom_url && @options.site_id.nil?
23
- logger.fatal "The site-id and custom-url options are both required."
24
- exit 2
25
- end
26
-
27
- if @options.import_file_name && @options.import_name
28
- unless File.exists?(@options.import_file_name)
29
- logger.fatal "Input file: #{@options.import_file_name} not found"
30
- exit 2
31
- end
32
-
33
- delete_generator(@options.import_name)
34
- CSVImporter.new(@options.import_name, @options.import_file_name, @options.token).import
35
- elsif @options.import_file_name || @options.import_name
36
- logger.fatal "You must pass both --import-variable-csv-file and --import-variable-name"
37
- exit 2
38
- end
39
-
40
- post_opts = {}
41
-
42
- if @options.git_trigger?
43
- logger.debug "Checking last git commit message:"
44
- commit_message = GitTrigger.last_commit_message
45
- logger.debug commit_message
46
-
47
- # Show some messages to users about tests/tags being overriden
48
- unless @options.tags.empty?
49
- logger.warn "Specified tags are ignored when using --git-trigger"
50
- else
51
- logger.warn "Specified tests are ignored when using --git-trigger"
52
- end
53
-
54
- if GitTrigger.git_trigger_should_run?(commit_message)
55
- tags = GitTrigger.extract_hashtags(commit_message)
56
- if tags.empty?
57
- logger.error "Triggered via git, but no hashtags detected. Please use commit message format:"
58
- logger.error "\t'some message. @rainforest #tag1 #tag2"
59
- exit 2
60
- else
61
- post_opts[:tags] = [tags.join(',')]
62
- end
63
- else
64
- logger.info "Not triggering as @rainforest was not mentioned in last commit message."
65
- exit 0
66
- end
67
- else
68
- # Not using git_trigger, so look for the
69
- if !@options.tags.empty?
70
- post_opts[:tags] = @options.tags
71
- else
72
- post_opts[:tests] = @options.tests
73
- end
74
- end
75
-
76
- post_opts[:conflict] = @options.conflict if @options.conflict
77
- post_opts[:browsers] = @options.browsers if @options.browsers
78
- post_opts[:site_id] = @options.site_id if @options.site_id
79
- post_opts[:gem_version] = Rainforest::Cli::VERSION
80
-
81
- post_opts[:environment_id] = get_environment_id(@options.custom_url) if @options.custom_url
82
-
83
- logger.debug "POST options: #{post_opts.inspect}"
84
- logger.info "Issuing run"
85
-
86
- response = post(API_URL + '/runs', post_opts)
87
-
88
- if response['error']
89
- logger.fatal "Error starting your run: #{response['error']}"
90
- exit 1
91
- end
92
-
93
- run_id = response["id"]
94
- running = true
95
-
96
- return unless @options.foreground?
97
-
98
- while running
99
- Kernel.sleep 5
100
- response = get "#{API_URL}/runs/#{run_id}?gem_version=#{Rainforest::Cli::VERSION}"
101
- if response
102
- if %w(queued validating in_progress sending_webhook waiting_for_callback).include?(response["state"])
103
- logger.info "Run #{run_id} is #{response['state']} and is #{response['current_progress']['percent']}% complete"
104
- running = false if response["result"] == 'failed' && @options.failfast?
105
- else
106
- logger.info "Run #{run_id} is now #{response["state"]} and has #{response["result"]}"
107
- running = false
108
- end
109
- end
110
- end
111
-
112
- if response["result"] != "passed"
113
- exit 1
114
- end
115
- true
116
- end
117
-
118
- def self.list_generators
119
- get("#{API_URL}/generators")
120
- end
121
-
122
- def self.delete_generator(name)
123
- generator = list_generators.find {|g| g['generator_type'] == 'tabular' && g['name'] == name }
124
- delete("#{API_URL}/generators/#{generator['id']}") if generator
125
- end
126
-
127
- def self.delete(url, body = {})
128
- response = HTTParty.delete url, {
129
- body: body,
130
- headers: {"CLIENT_TOKEN" => @options.token}
131
- }
132
-
133
- JSON.parse(response.body)
134
- end
135
-
136
- def self.post(url, body = {})
137
- response = HTTParty.post url, {
138
- body: body,
139
- headers: {"CLIENT_TOKEN" => @options.token}
140
- }
141
-
142
- JSON.parse(response.body)
143
- end
144
-
145
- def self.get(url, body = {})
146
- response = HTTParty.get url, {
147
- body: body,
148
- headers: {"CLIENT_TOKEN" => @options.token}
149
- }
150
-
151
- if response.code == 200
152
- JSON.parse(response.body)
153
- else
154
- nil
155
- end
156
- end
157
-
158
- def self.get_environment_id url
159
17
  begin
160
- URI.parse(url)
161
- rescue URI::InvalidURIError
162
- logger.fatal "The custom URL is invalid"
18
+ options.validate!
19
+ rescue OptionParser::ValidationError => e
20
+ logger.fatal e.message
163
21
  exit 2
164
22
  end
165
23
 
166
- env_post_body = { name: 'temporary-env-for-custom-url-via-CLI', url: url }
167
- environment = post("#{API_URL}/environments", env_post_body)
168
-
169
- if environment['error']
170
- # I am talking about a URL here because the environments are pretty
171
- # much hidden from clients so far.
172
- logger.fatal "Error creating the ad-hoc URL: #{environment['error']}"
173
- exit 1
174
- end
24
+ runner = Runner.new(options)
25
+ runner.run
175
26
 
176
- return environment['id']
27
+ true
177
28
  end
178
29
 
179
30
  def self.logger
180
31
  @logger ||= Logger.new(STDOUT)
181
32
  end
33
+
34
+ def self.logger=(logger)
35
+ @logger = logger
36
+ end
182
37
  end
183
38
  end
@@ -16,4 +16,4 @@ module Rainforest
16
16
  end
17
17
  end
18
18
  end
19
- end
19
+ end
@@ -0,0 +1,56 @@
1
+ module Rainforest
2
+ module Cli
3
+ class HttpClient
4
+ API_URL = ENV.fetch("RAINFOREST_API_URL") do
5
+ 'https://app.rainforestqa.com/api/1'
6
+ end.freeze
7
+
8
+ def initialize(options)
9
+ @token = options.fetch(:token)
10
+ end
11
+
12
+ def delete(url, body = {})
13
+ response = HTTParty.delete make_url(url), {
14
+ body: body,
15
+ headers: headers,
16
+ }
17
+
18
+ JSON.parse(response.body)
19
+ end
20
+
21
+ def post(url, body = {})
22
+ response = HTTParty.post make_url(url), {
23
+ body: body,
24
+ headers: headers,
25
+ }
26
+
27
+ JSON.parse(response.body)
28
+ end
29
+
30
+ def get(url, body = {})
31
+ response = HTTParty.get make_url(url), {
32
+ body: body,
33
+ headers: headers,
34
+ }
35
+
36
+ if response.code == 200
37
+ JSON.parse(response.body)
38
+ else
39
+ nil
40
+ end
41
+ end
42
+
43
+ private
44
+ def make_url(url)
45
+ File.join(API_URL, url)
46
+ end
47
+
48
+ def headers
49
+ {
50
+ "CLIENT_TOKEN" => @token,
51
+ "User-Agent" => "Rainforest-cli-#{Rainforest::Cli::VERSION}"
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -66,7 +66,7 @@ module Rainforest
66
66
  opts.on("--custom-url URL", String, "Use a custom url for this run. You will need to specify a site_id too for this to work.") do |value|
67
67
  @custom_url = value
68
68
  end
69
-
69
+
70
70
  opts.on_tail("--help", "Display help message and exit") do |value|
71
71
  puts opts
72
72
  exit 0
@@ -92,6 +92,29 @@ module Rainforest
92
92
  def foreground?
93
93
  @foreground
94
94
  end
95
+
96
+ def validate!
97
+ unless token
98
+ raise ValidationError, "You must pass your API token using: --token TOKEN"
99
+ end
100
+
101
+ if custom_url && site_id.nil?
102
+ raise ValidationError, "The site-id and custom-url options are both required."
103
+ end
104
+
105
+ if import_file_name && import_name
106
+ unless File.exists?(import_file_name)
107
+ raise ValidationError, "Input file: #{import_file_name} not found"
108
+ end
109
+
110
+ elsif import_file_name || import_name
111
+ raise ValidationError, "You must pass both --import-variable-csv-file and --import-variable-name"
112
+ end
113
+ true
114
+ end
115
+
116
+ class ValidationError < RuntimeError
117
+ end
95
118
  end
96
119
  end
97
120
  end
@@ -0,0 +1,151 @@
1
+ module Rainforest
2
+ module Cli
3
+ class Runner
4
+ attr_reader :options, :client
5
+
6
+ def initialize(options)
7
+ @options = options
8
+ @client = HttpClient.new token: options.token
9
+ end
10
+
11
+ def run
12
+ if options.import_file_name && options.import_name
13
+ delete_generator(options.import_name)
14
+ CSVImporter.new(options.import_name, options.import_file_name, options.token).import
15
+ end
16
+
17
+ post_opts = make_create_run_options
18
+
19
+ logger.debug "POST options: #{post_opts.inspect}"
20
+ logger.info "Issuing run"
21
+
22
+ response = client.post('/runs', post_opts)
23
+
24
+ if response['error']
25
+ logger.fatal "Error starting your run: #{response['error']}"
26
+ exit 1
27
+ end
28
+
29
+
30
+ if options.foreground?
31
+ run_id = response.fetch("id")
32
+ wait_for_run_completion(run_id)
33
+ else
34
+ true
35
+ end
36
+ end
37
+
38
+ def wait_for_run_completion(run_id)
39
+ running = true
40
+ while running
41
+ Kernel.sleep 5
42
+ response = client.get("/runs/#{run_id}")
43
+ if response
44
+ state_details = response.fetch('state_details')
45
+ unless state_details.fetch("is_final_state")
46
+ logger.info "Run #{run_id} is #{response['state']} and is #{response['current_progress']['percent']}% complete"
47
+ running = false if response["result"] == 'failed' && options.failfast?
48
+ else
49
+ logger.info "Run #{run_id} is now #{response["state"]} and has #{response["result"]}"
50
+ running = false
51
+ end
52
+ end
53
+ end
54
+
55
+ if url = response["frontend_url"]
56
+ logger.info "The detailed results are available at #{url}"
57
+ end
58
+
59
+ if response["result"] != "passed"
60
+ exit 1
61
+ end
62
+ end
63
+
64
+ def make_create_run_options
65
+ post_opts = {}
66
+ if options.git_trigger?
67
+ logger.debug "Checking last git commit message:"
68
+ commit_message = GitTrigger.last_commit_message
69
+ logger.debug commit_message
70
+
71
+ # Show some messages to users about tests/tags being overriden
72
+ unless options.tags.empty?
73
+ logger.warn "Specified tags are ignored when using --git-trigger"
74
+ else
75
+ logger.warn "Specified tests are ignored when using --git-trigger"
76
+ end
77
+
78
+ if GitTrigger.git_trigger_should_run?(commit_message)
79
+ tags = GitTrigger.extract_hashtags(commit_message)
80
+ if tags.empty?
81
+ logger.error "Triggered via git, but no hashtags detected. Please use commit message format:"
82
+ logger.error "\t'some message. @rainforest #tag1 #tag2"
83
+ exit 2
84
+ else
85
+ post_opts[:tags] = [tags.join(',')]
86
+ end
87
+ else
88
+ logger.info "Not triggering as @rainforest was not mentioned in last commit message."
89
+ exit 0
90
+ end
91
+ else
92
+ # Not using git_trigger, so look for the
93
+ if !options.tags.empty?
94
+ post_opts[:tags] = options.tags
95
+ else
96
+ post_opts[:tests] = options.tests
97
+ end
98
+ end
99
+
100
+ post_opts[:conflict] = options.conflict if options.conflict
101
+ post_opts[:browsers] = options.browsers if options.browsers
102
+ post_opts[:site_id] = options.site_id if options.site_id
103
+
104
+ if options.custom_url
105
+ post_opts[:environment_id] = get_environment_id(options.custom_url)
106
+ end
107
+
108
+ post_opts
109
+ end
110
+
111
+ def logger
112
+ Rainforest::Cli.logger
113
+ end
114
+
115
+ def list_generators
116
+ client.get("/generators")
117
+ end
118
+
119
+ def delete_generator(name)
120
+ generator = list_generators.find {|g| g['generator_type'] == 'tabular' && g['name'] == name }
121
+ client.delete("generators/#{generator['id']}") if generator
122
+ end
123
+
124
+ def url_valid?(url)
125
+ return false unless URI::regexp === url
126
+
127
+ uri = URI.parse(url)
128
+ %w(http https).include?(uri.scheme)
129
+ end
130
+
131
+ def get_environment_id url
132
+ unless url_valid?(url)
133
+ logger.fatal "The custom URL is invalid"
134
+ exit 2
135
+ end
136
+
137
+ env_post_body = { name: 'temporary-env-for-custom-url-via-CLI', url: url }
138
+ environment = client.post("/environments", env_post_body)
139
+
140
+ if environment['error']
141
+ # I am talking about a URL here because the environments are pretty
142
+ # much hidden from clients so far.
143
+ logger.fatal "Error creating the ad-hoc URL: #{environment['error']}"
144
+ exit 1
145
+ end
146
+
147
+ return environment['id']
148
+ end
149
+ end
150
+ end
151
+ end
@@ -1,5 +1,5 @@
1
1
  module Rainforest
2
2
  module Cli
3
- VERSION = "0.0.17"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  end
data/spec/cli_spec.rb CHANGED
@@ -1,52 +1,38 @@
1
1
  describe Rainforest::Cli do
2
+ let(:http_client) { Rainforest::Cli::HttpClient.any_instance }
3
+
2
4
  before do
3
5
  Kernel.stub(:sleep)
4
- stub_const("Rainforest::Cli::API_URL", 'http://app.rainforest.dev/api/1')
5
6
  end
6
7
 
7
8
  describe ".start" do
8
9
  let(:valid_args) { %w(--token foo run --fg all) }
9
- let(:ok_progress) { {"state" => "in_progress", "current_progress" => {"percent" => "1"} } }
10
+ let(:ok_progress) do
11
+ {
12
+ "state" => "in_progress",
13
+ "current_progress" => {"percent" => "1"},
14
+ "state_details" => { "is_final_state" => false },
15
+ "result" => "no_result",
16
+ }
17
+ end
18
+
19
+ let(:complete_response) do
20
+ {
21
+ "state" => "complete",
22
+ "current_progress" => {"percent" => "100"},
23
+ "state_details" => { "is_final_state" => true},
24
+ "result" => "passed",
25
+ }
26
+ end
10
27
 
11
28
  context "with bad parameters" do
12
29
  context "no token" do
13
30
  let(:params) { %w(--custom-url http://ad-hoc.example.com) }
14
31
  it 'errors out' do
15
32
  expect_any_instance_of(Logger).to receive(:fatal).with('You must pass your API token using: --token TOKEN')
16
- begin
17
- described_class.start(params)
18
- rescue SystemExit => e
19
- # That's fine, this is expected but tested in a differnet assertion
20
- end
21
- end
22
-
23
- it 'exits with exit code 2' do
24
- expect {
25
- described_class.start(params)
26
- }.to raise_error { |error|
27
- expect(error).to be_a(SystemExit)
28
- expect(error.status).to eq 2
29
- }
30
- end
31
- end
32
-
33
- context "with custom-url with no site-id" do
34
- let(:params) { %w(--token x --custom-url http://ad-hoc.example.com) }
35
-
36
- it 'errors out' do
37
- expect_any_instance_of(Logger).to receive(:fatal).with('The site-id and custom-url options are both required.')
38
- begin
39
- described_class.start(params)
40
- rescue SystemExit => e
41
- # That's fine, this is expected but tested in a differnet assertion
42
- end
43
- end
44
-
45
- it 'exits with exit code 2' do
46
33
  expect {
47
34
  described_class.start(params)
48
- }.to raise_error { |error|
49
- expect(error).to be_a(SystemExit)
35
+ }.to raise_error(SystemExit) { |error|
50
36
  expect(error.status).to eq 2
51
37
  }
52
38
  end
@@ -68,23 +54,23 @@ describe Rainforest::Cli do
68
54
  before do
69
55
  Rainforest::Cli::GitTrigger.stub(:last_commit_message) { commit_message }
70
56
  end
71
-
57
+
72
58
  describe "with tags parameter passed" do
73
59
  let(:params) { %w(--token x --tag x --git-trigger) }
74
60
 
75
61
  it "warns about the parameter being ignored" do
76
62
  expect_any_instance_of(Logger).to receive(:warn).with("Specified tags are ignored when using --git-trigger")
77
-
63
+
78
64
  start_with_params(params, 0)
79
65
  end
80
66
  end
81
-
67
+
82
68
  describe "with tags parameter passed" do
83
69
  let(:params) { %w(all --token x --git-trigger) }
84
70
 
85
71
  it "warns about the parameter being ignored" do
86
72
  expect_any_instance_of(Logger).to receive(:warn).with("Specified tests are ignored when using --git-trigger")
87
-
73
+
88
74
  start_with_params(params, 0)
89
75
  end
90
76
  end
@@ -102,7 +88,7 @@ describe Rainforest::Cli do
102
88
  it "exit 2's and logs the reason" do
103
89
  expect_any_instance_of(Logger).to receive(:error).with("Triggered via git, but no hashtags detected. Please use commit message format:")
104
90
  expect_any_instance_of(Logger).to receive(:error).with("\t'some message. @rainforest #tag1 #tag2")
105
-
91
+
106
92
  start_with_params(params, 2)
107
93
  end
108
94
  end
@@ -111,10 +97,11 @@ describe Rainforest::Cli do
111
97
  let(:commit_message) { 'a test commit message @rainforest #run-me' }
112
98
 
113
99
  it "starts the run with the specified tags" do
114
- expect(described_class).to receive(:post).with(
115
- "http://app.rainforest.dev/api/1/runs",
116
- { :tags=>['run-me'], :gem_version=>Rainforest::Cli::VERSION }
117
- ).and_return( {} )
100
+ http_client.should_receive(:post) do |url, options|
101
+ expect(url).to eq("/runs")
102
+ expect(options[:tags]).to eq(['run-me'])
103
+ {}
104
+ end
118
105
 
119
106
  start_with_params(params, 0)
120
107
  end
@@ -124,9 +111,7 @@ describe Rainforest::Cli do
124
111
  context "with site-id and custom-url" do
125
112
  let(:params) { %w(--token x --site 3 --custom-url http://ad-hoc.example.com) }
126
113
  it "creates a new environment" do
127
- allow(described_class).to receive(:post).and_return { exit }
128
- expect(described_class).to receive(:post).with(
129
- "http://app.rainforest.dev/api/1/environments",
114
+ http_client.should_receive(:post).with("/environments",
130
115
  {
131
116
  :name => "temporary-env-for-custom-url-via-CLI",
132
117
  :url=>"http://ad-hoc.example.com"
@@ -135,6 +120,8 @@ describe Rainforest::Cli do
135
120
  { 'id' => 333 }
136
121
  )
137
122
 
123
+ http_client.should_receive(:post).with("/runs", anything).and_return( { "id" => 1 } )
124
+
138
125
  # This is a hack because when expecting a function to be called with
139
126
  # parameters, the last call is compared but I want to compare the first
140
127
  # call, not the call to create a run, so I exit, but rescue from it here
@@ -147,11 +134,11 @@ describe Rainforest::Cli do
147
134
  end
148
135
 
149
136
  it "starts the run with site_id and environment_id" do
150
- allow(described_class).to receive(:get_environment_id).and_return(333)
137
+ Rainforest::Cli::Runner.any_instance.stub(get_environment_id: 333)
151
138
 
152
- expect(described_class).to receive(:post).with(
153
- "http://app.rainforest.dev/api/1/runs",
154
- { :tests=>[], :site_id=>3, :gem_version=>Rainforest::Cli::VERSION, :environment_id=>333 }
139
+ http_client.should_receive(:post).with(
140
+ "/runs",
141
+ { :tests=>[], :site_id=>3, :environment_id=>333 }
155
142
  ).and_return( {} )
156
143
  described_class.start(params)
157
144
  end
@@ -159,11 +146,11 @@ describe Rainforest::Cli do
159
146
 
160
147
  context "a simple run" do
161
148
  before do
162
- described_class.stub(:post) { {"id" => 1} }
163
- 3.times do
164
- described_class.should_receive(:get) { ok_progress }
149
+ http_client.stub(:post) { {"id" => 1} }
150
+ 3.times do
151
+ http_client.should_receive(:get) { ok_progress }
165
152
  end
166
- described_class.should_receive(:get) { {"state" => "complete", "result" => "passed" } }
153
+ http_client.should_receive(:get) { complete_response }
167
154
  end
168
155
 
169
156
  it "should return true" do
@@ -173,18 +160,18 @@ describe Rainforest::Cli do
173
160
 
174
161
  context "a run where the server 500s after a while" do
175
162
  before do
176
- described_class.stub(:post) { {"id" => 1} }
177
- 2.times do
178
- described_class.should_receive(:get) { ok_progress }
163
+ http_client.stub(:post) { {"id" => 1} }
164
+ 2.times do
165
+ http_client.should_receive(:get) { ok_progress }
179
166
  end
180
167
 
181
- described_class.should_receive(:get) { nil }
168
+ http_client.should_receive(:get) { nil }
182
169
 
183
- 2.times do
184
- described_class.should_receive(:get) { ok_progress }
170
+ 2.times do
171
+ http_client.should_receive(:get) { ok_progress }
185
172
  end
186
173
 
187
- described_class.should_receive(:get) { {"state" => "complete", "result" => "passed" } }
174
+ http_client.should_receive(:get) { complete_response }
188
175
  end
189
176
 
190
177
  it "should return true" do
@@ -192,34 +179,4 @@ describe Rainforest::Cli do
192
179
  end
193
180
  end
194
181
  end
195
-
196
- describe ".get_environment_id" do
197
- context "with an invalid URL" do
198
- xit 'errors out and exits' do
199
- expect_any_instance_of(Logger).to receive(:fatal).with("The custom URL is invalid")
200
- expect {
201
- described_class.get_environment_id('http://some=weird')
202
- }.to raise_error { |error|
203
- expect(error).to be_a(SystemExit)
204
- expect(error.status).to eq 2
205
- }
206
- end
207
- end
208
-
209
- context 'on API error' do
210
- before do
211
- allow(described_class).to receive(:post).and_return( {"error"=>"Some API error"} )
212
- end
213
-
214
- it 'errors out and exits' do
215
- expect_any_instance_of(Logger).to receive(:fatal).with("Error creating the ad-hoc URL: Some API error")
216
- expect {
217
- described_class.get_environment_id('http://example.com')
218
- }.to raise_error { |error|
219
- expect(error).to be_a(SystemExit)
220
- expect(error.status).to eq 1
221
- }
222
- end
223
- end
224
- end
225
182
  end
@@ -1,13 +1,25 @@
1
1
  describe Rainforest::Cli::GitTrigger do
2
2
  subject { described_class }
3
3
 
4
+ let(:default_dir) { __dir__ }
5
+ let(:test_repo_dir) { File.join(Dir.tmpdir, 'raiforest-cli', 'test-repo') }
6
+
4
7
  describe ".last_commit_message" do
5
- xit "returns a string" do
8
+ around(:each) do |spec|
6
9
  default_dir = Dir.pwd
10
+ FileUtils.mkdir_p test_repo_dir
11
+ Dir.chdir(test_repo_dir)
12
+ setup_test_repo
13
+ begin
14
+ spec.call
15
+ ensure
16
+ Dir.chdir(default_dir)
17
+ end
18
+ end
7
19
 
8
- Dir.chdir(File.join([default_dir, 'spec', 'test-repo']))
20
+ # Commented because it kills CI
21
+ xit "returns a string" do
9
22
  expect(described_class.last_commit_message).to eq "Initial commit"
10
- Dir.chdir(default_dir)
11
23
  end
12
24
  end
13
25
 
@@ -26,4 +38,14 @@ describe Rainforest::Cli::GitTrigger do
26
38
  expect(described_class.extract_hashtags('#dashes-work, #underscores_work #007')).to eq ['dashes-work', 'underscores_work', '007']
27
39
  end
28
40
  end
41
+
42
+ def setup_test_repo
43
+ FileUtils.rm_rf File.join(test_repo_dir, "*")
44
+ [
45
+ "git init",
46
+ "git commit --allow-empty -m 'Initial commit'",
47
+ ].each do |cmd|
48
+ system cmd
49
+ end
50
+ end
29
51
  end
data/spec/options_spec.rb CHANGED
@@ -1,80 +1,128 @@
1
1
  describe Rainforest::Cli::OptionParser do
2
2
  subject { Rainforest::Cli::OptionParser.new(args) }
3
3
 
4
- context "importing csv file" do
5
- let(:args) { ["--import-variable-csv-file", "some_file.csv"] }
6
- its(:import_file_name) { should == "some_file.csv" }
7
- end
4
+ describe "#initialize" do
5
+ context "importing csv file" do
6
+ let(:args) { ["--import-variable-csv-file", "some_file.csv"] }
7
+ its(:import_file_name) { should == "some_file.csv" }
8
+ end
8
9
 
9
- context "importing name" do
10
- let(:args) { ["--import-variable-name", "some_name"] }
11
- its(:import_name) { should == "some_name" }
12
- end
10
+ context "importing name" do
11
+ let(:args) { ["--import-variable-name", "some_name"] }
12
+ its(:import_name) { should == "some_name" }
13
+ end
13
14
 
14
- context "run all tests" do
15
- let(:args) { ["run", "all"] }
16
- its(:tests) { should == ["all"]}
17
- its(:conflict) { should == nil}
18
- end
15
+ context "run all tests" do
16
+ let(:args) { ["run", "all"] }
17
+ its(:tests) { should == ["all"]}
18
+ its(:conflict) { should == nil}
19
+ end
19
20
 
20
- context "run from tags" do
21
- let(:args) { ["run", "--tag", "run-me"] }
22
- its(:tests) { should == []}
23
- its(:tags) { should == ["run-me"]}
24
- end
21
+ context "run from tags" do
22
+ let(:args) { ["run", "--tag", "run-me"] }
23
+ its(:tests) { should == []}
24
+ its(:tags) { should == ["run-me"]}
25
+ end
25
26
 
26
- context "only run in specific browsers" do
27
- let(:args) { ["run", "--browsers", "ie8"] }
28
- its(:browsers) { should == ["ie8"]}
29
- end
27
+ context "only run in specific browsers" do
28
+ let(:args) { ["run", "--browsers", "ie8"] }
29
+ its(:browsers) { should == ["ie8"]}
30
+ end
30
31
 
31
- context "raises errors with invalid browsers" do
32
- let(:args) { ["run", "--browsers", "lulbrower"] }
32
+ context "raises errors with invalid browsers" do
33
+ let(:args) { ["run", "--browsers", "lulbrower"] }
33
34
 
34
- it "raises an exception" do
35
- expect{subject}.to raise_error(Rainforest::Cli::BrowserException)
35
+ it "raises an exception" do
36
+ expect{subject}.to raise_error(Rainforest::Cli::BrowserException)
37
+ end
36
38
  end
37
- end
38
39
 
39
- context "accepts multiple browsers" do
40
- let(:args) { ["run", "--browsers", "ie8,chrome"] }
41
- its(:browsers) { should == ["ie8", "chrome"]}
42
- end
40
+ context "accepts multiple browsers" do
41
+ let(:args) { ["run", "--browsers", "ie8,chrome"] }
42
+ its(:browsers) { should == ["ie8", "chrome"]}
43
+ end
43
44
 
44
- context "it parses the --git-trigger flag" do
45
- let(:args) { ["run", "--git-trigger", "all"] }
46
- its(:tests) { should == ["all"]}
47
- its(:git_trigger?) { should be_true }
48
- end
45
+ context "it parses the --git-trigger flag" do
46
+ let(:args) { ["run", "--git-trigger", "all"] }
47
+ its(:tests) { should == ["all"]}
48
+ its(:git_trigger?) { should be_true }
49
+ end
49
50
 
50
- context "it parses the --fg flag" do
51
- let(:args) { ["run", "--fg", "all"] }
52
- its(:tests) { should == ["all"]}
53
- its(:foreground?) { should be_true }
54
- end
51
+ context "it parses the --fg flag" do
52
+ let(:args) { ["run", "--fg", "all"] }
53
+ its(:tests) { should == ["all"]}
54
+ its(:foreground?) { should be_true }
55
+ end
55
56
 
56
- context "it parses the api token" do
57
- let(:args) { ["run", "--token", "abc", "all"] }
58
- its(:token) { should == "abc"}
59
- end
57
+ context "it parses the api token" do
58
+ let(:args) { ["run", "--token", "abc", "all"] }
59
+ its(:token) { should == "abc"}
60
+ end
60
61
 
61
- context "it parses the conflict flag" do
62
- let(:args) { ["run", "--conflict", "abort", "all"] }
63
- its(:conflict) { should == "abort"}
64
- end
62
+ context "it parses the conflict flag" do
63
+ let(:args) { ["run", "--conflict", "abort", "all"] }
64
+ its(:conflict) { should == "abort"}
65
+ end
65
66
 
66
- context "it parses the fail-fast flag" do
67
- let(:args) { ["run", "--fail-fast"] }
68
- its(:failfast?) { should be_true }
69
- end
67
+ context "it parses the fail-fast flag" do
68
+ let(:args) { ["run", "--fail-fast"] }
69
+ its(:failfast?) { should be_true }
70
+ end
71
+
72
+ context "it parses the site-id flag" do
73
+ let(:args) { ["run", "--site-id", '3'] }
74
+ its(:site_id) { should eq 3 }
75
+ end
70
76
 
71
- context "it parses the site-id flag" do
72
- let(:args) { ["run", "--site-id", '3'] }
73
- its(:site_id) { should eq 3 }
77
+ context "it parses the custom-url flag" do
78
+ let(:args) { ["run", "--custom-url", 'http://ad-hoc.example.com'] }
79
+ its(:custom_url) { should eq 'http://ad-hoc.example.com' }
80
+ end
74
81
  end
75
82
 
76
- context "it parses the custom-url flag" do
77
- let(:args) { ["run", "--custom-url", 'http://ad-hoc.example.com'] }
78
- its(:custom_url) { should eq 'http://ad-hoc.example.com' }
83
+ describe "#validate!" do
84
+ def does_not_raise_a_validation_exception
85
+ expect do
86
+ subject.validate!
87
+ end.to_not raise_error
88
+ end
89
+
90
+ def raises_a_validation_exception
91
+ expect do
92
+ subject.validate!
93
+ end.to raise_error(described_class::ValidationError)
94
+ end
95
+
96
+ context "with valid arguments" do
97
+ let(:args) { %w(--token foo) }
98
+ it { does_not_raise_a_validation_exception }
99
+ end
100
+
101
+ context "with missing token" do
102
+ let(:args) { %w() }
103
+ it { raises_a_validation_exception }
104
+ end
105
+
106
+ context "with a custom url but no site id" do
107
+ let(:args) { %w(--token foo --custom-url http://www.example.com) }
108
+ it { raises_a_validation_exception }
109
+ end
110
+
111
+ context "with a import_file_name but no import name" do
112
+ let(:args) { %w(--token foo --import-variable-csv-file foo.csv) }
113
+ it { raises_a_validation_exception }
114
+ end
115
+
116
+ context "with a import_file_name and a import_name" do
117
+ context "for an existing file" do
118
+ let(:args) { %W(--token foo --import-variable-csv-file #{__FILE__} --import-variable-name my-var) }
119
+ it { does_not_raise_a_validation_exception }
120
+ end
121
+
122
+ context "for a non existing file" do
123
+ let(:args) { %W(--token foo --import-variable-csv-file does_not_exist --import-variable-name my-var) }
124
+ it { raises_a_validation_exception }
125
+ end
126
+ end
79
127
  end
80
128
  end
@@ -0,0 +1,59 @@
1
+ describe Rainforest::Cli::Runner do
2
+ let(:args) { %w(run all) }
3
+ let(:options) { Rainforest::Cli::OptionParser.new(args) }
4
+ subject { described_class.new(options) }
5
+
6
+ describe "#get_environment_id" do
7
+ context "with an invalid URL" do
8
+ it 'errors out and exits' do
9
+ expect {
10
+ subject.get_environment_id('some=weird')
11
+ }.to raise_error(SystemExit) { |error|
12
+ expect(error.status).to eq 2
13
+ }
14
+ end
15
+ end
16
+
17
+ context 'on API error' do
18
+ before do
19
+ allow(subject.client).to receive(:post).and_return( {"error"=>"Some API error"} )
20
+ end
21
+
22
+ it 'errors out and exits' do
23
+ expect_any_instance_of(Logger).to receive(:fatal).with("Error creating the ad-hoc URL: Some API error")
24
+ expect {
25
+ subject.get_environment_id('http://example.com')
26
+ }.to raise_error(SystemExit) { |error|
27
+ expect(error.status).to eq 1
28
+ }
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "#url_valid?" do
34
+ subject { super().url_valid?(url) }
35
+ [
36
+ "http://example.org",
37
+ "https://example.org",
38
+ "http://example.org/",
39
+ "http://example.org?foo=bar",
40
+ ].each do |valid_url|
41
+ context "#{valid_url}" do
42
+ let(:url) { valid_url }
43
+ it { should be(true) }
44
+ end
45
+ end
46
+
47
+ [
48
+ "ftp://example.org",
49
+ "example.org",
50
+ "",
51
+ ].each do |valid_url|
52
+ context "#{valid_url}" do
53
+ let(:url) { valid_url }
54
+ it { should be(false) }
55
+ end
56
+ end
57
+ end
58
+
59
+ end
data/spec/spec_helper.rb CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  require "rainforest/cli"
9
9
 
10
+ Rainforest::Cli.logger = Logger.new(StringIO.new)
11
+
10
12
  RSpec.configure do |config|
11
13
  config.treat_symbols_as_metadata_keys_with_true_values = true
12
14
  config.run_all_when_everything_filtered = true
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rainforest-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.17
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Mathieu
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-01-22 00:00:00.000000000 Z
12
+ date: 2015-01-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: httparty
@@ -105,12 +105,15 @@ files:
105
105
  - lib/rainforest/cli.rb
106
106
  - lib/rainforest/cli/csv_importer.rb
107
107
  - lib/rainforest/cli/git_trigger.rb
108
+ - lib/rainforest/cli/http_client.rb
108
109
  - lib/rainforest/cli/options.rb
110
+ - lib/rainforest/cli/runner.rb
109
111
  - lib/rainforest/cli/version.rb
110
112
  - rainforest-cli.gemspec
111
113
  - spec/cli_spec.rb
112
114
  - spec/git_trigger_spec.rb
113
115
  - spec/options_spec.rb
116
+ - spec/runner_spec.rb
114
117
  - spec/spec_helper.rb
115
118
  homepage: https://www.rainforestqa.com/
116
119
  licenses:
@@ -140,4 +143,5 @@ test_files:
140
143
  - spec/cli_spec.rb
141
144
  - spec/git_trigger_spec.rb
142
145
  - spec/options_spec.rb
146
+ - spec/runner_spec.rb
143
147
  - spec/spec_helper.rb