rainforest-cli 0.0.17 → 1.0.0

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