rainforest-cli 1.2.0 → 1.2.1
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 +4 -4
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +5 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +7 -1
- data/README.md +2 -0
- data/Rakefile +6 -4
- data/circle.yml +3 -0
- data/lib/rainforest/cli.rb +20 -16
- data/lib/rainforest/cli/constants.rb +4 -0
- data/lib/rainforest/cli/csv_importer.rb +6 -6
- data/lib/rainforest/cli/git_trigger.rb +2 -1
- data/lib/rainforest/cli/http_client.rb +50 -13
- data/lib/rainforest/cli/options.rb +71 -39
- data/lib/rainforest/cli/remote_tests.rb +49 -0
- data/lib/rainforest/cli/runner.rb +19 -17
- data/lib/rainforest/cli/test_files.rb +32 -14
- data/lib/rainforest/cli/test_importer.rb +35 -155
- data/lib/rainforest/cli/test_parser.rb +38 -14
- data/lib/rainforest/cli/uploader.rb +107 -0
- data/lib/rainforest/cli/validator.rb +158 -0
- data/lib/rainforest/cli/version.rb +2 -1
- data/rainforest-cli.gemspec +14 -12
- data/spec/cli_spec.rb +84 -90
- data/spec/csv_importer_spec.rb +13 -8
- data/spec/git_trigger_spec.rb +28 -15
- data/spec/http_client_spec.rb +57 -0
- data/spec/options_spec.rb +72 -70
- data/spec/rainforest-example/example_test.rfml +2 -1
- data/spec/remote_tests_spec.rb +22 -0
- data/spec/runner_spec.rb +17 -16
- data/spec/spec_helper.rb +16 -9
- data/spec/test_files_spec.rb +20 -24
- data/spec/uploader_spec.rb +54 -0
- data/spec/validation-examples/circular_embeds/test1.rfml +5 -0
- data/spec/validation-examples/circular_embeds/test2.rfml +5 -0
- data/spec/validation-examples/correct_embeds/embedded_test.rfml +6 -0
- data/spec/validation-examples/correct_embeds/test_with_embedded.rfml +8 -0
- data/spec/validation-examples/missing_embeds/correct_test.rfml +8 -0
- data/spec/validation-examples/missing_embeds/incorrect_test.rfml +8 -0
- data/spec/validation-examples/parse_errors/no_parse_errors.rfml +6 -0
- data/spec/validation-examples/parse_errors/no_question.rfml +5 -0
- data/spec/validation-examples/parse_errors/no_question_mark.rfml +6 -0
- data/spec/validation-examples/parse_errors/no_rfml_id.rfml +5 -0
- data/spec/validator_spec.rb +119 -0
- metadata +96 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04fa2b25294be20d03d2c6f41b74d19d806b0fe5
|
4
|
+
data.tar.gz: 5fe743cb8e0fae9c345c191d1e01f73705e3c24a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3bc25ec6f023a5d1078538d7368552a970a0079652bb04bfd20c9f5e9237b2d707a3ae88eb51ed82df87ed3188b8d921464adb0dcaf6a23f663b510156a75d2d
|
7
|
+
data.tar.gz: 510f8eff35dddb1f889e3afa2db044fc393db312d89bcbf22ea1db330bf1a03286fb9bdc3c8baf2280aba0bb7d4d4a9f4a44d6a99161ce92aae91161887045ae
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.3.0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Rainforest CLI Changelog
|
2
2
|
|
3
|
+
## 1.2.1 - 18th March 2016
|
4
|
+
- Fixed a bug where uploading was stuck in an infinite loop if an embedded id did not exist (7b02b2f66dbd47098a7c1d5f79bc60a0cbe8984f, @epaulet)
|
5
|
+
- Fixed a bug that occurred when specifying a nested test folder without creating parent folders first (6c1b0e02c858f9d9c264e771f964b3e1a4ea8c7e, @epaulet)
|
6
|
+
- Removed 'ro' tag and use 'rainforest-cli' as the test's source instead.
|
7
|
+
(10864a7e054d4c869f6a345608b2d1c1c0925fe8, @epaulet)
|
8
|
+
- Add retries on API calls so that minor interruptions do not cancel Rainforest builds.
|
9
|
+
(98021337a3fbbf16c7cd858bbec5d925fb86c939, @epaulet)
|
10
|
+
|
3
11
|
## 1.2.0 - 8th February 2016
|
4
12
|
- Add support for embedded tests.
|
5
13
|
- Add support for customizable RFML ids.
|
data/Gemfile
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
source 'https://rubygems.org'
|
2
3
|
|
3
4
|
# Specify your gem's dependencies in rainforest-cli.gemspec
|
4
5
|
gemspec
|
5
6
|
|
7
|
+
gem 'rf-stylez'
|
8
|
+
gem 'circlemator', require: false
|
9
|
+
|
6
10
|
group :test do
|
7
|
-
gem
|
11
|
+
gem 'rspec', '~> 3.4.0'
|
12
|
+
gem 'rspec-its', '~> 1.2.0'
|
13
|
+
gem 'byebug'
|
8
14
|
end
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
[](https://travis-ci.org/rainforestapp/rainforest-cli)
|
2
2
|
|
3
|
+
[](https://badge.fury.io/rb/rainforest-cli)
|
4
|
+
|
3
5
|
# Rainforest-cli
|
4
6
|
|
5
7
|
A command line interface to interact with RainforestQA.
|
data/Rakefile
CHANGED
@@ -1,11 +1,13 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'bundler/gem_tasks'
|
2
3
|
|
3
4
|
begin
|
4
5
|
require 'rspec/core/rake_task'
|
5
6
|
|
6
7
|
RSpec::Core::RakeTask.new(:spec)
|
7
8
|
|
8
|
-
task :
|
9
|
-
rescue LoadError
|
9
|
+
task default: :spec
|
10
|
+
rescue LoadError => e
|
11
|
+
puts e.message
|
12
|
+
exit
|
10
13
|
end
|
11
|
-
|
data/circle.yml
ADDED
data/lib/rainforest/cli.rb
CHANGED
@@ -1,16 +1,20 @@
|
|
1
|
-
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'erb'
|
3
|
+
require 'json'
|
4
|
+
require 'logger'
|
5
|
+
require 'rainforest/cli/version'
|
6
|
+
require 'rainforest/cli/constants'
|
7
|
+
require 'rainforest/cli/options'
|
8
|
+
require 'rainforest/cli/runner'
|
9
|
+
require 'rainforest/cli/http_client'
|
10
|
+
require 'rainforest/cli/git_trigger'
|
11
|
+
require 'rainforest/cli/csv_importer'
|
12
|
+
require 'rainforest/cli/test_parser'
|
13
|
+
require 'rainforest/cli/test_files'
|
14
|
+
require 'rainforest/cli/remote_tests'
|
15
|
+
require 'rainforest/cli/validator'
|
16
|
+
require 'rainforest/cli/test_importer'
|
17
|
+
require 'rainforest/cli/uploader'
|
14
18
|
|
15
19
|
module RainforestCli
|
16
20
|
def self.start(args)
|
@@ -32,16 +36,16 @@ module RainforestCli
|
|
32
36
|
t = TestImporter.new(options)
|
33
37
|
t.create_new
|
34
38
|
when 'validate'
|
35
|
-
t =
|
39
|
+
t = Validator.new(options)
|
36
40
|
t.validate
|
37
41
|
when 'upload'
|
38
|
-
t =
|
42
|
+
t = Uploader.new(options)
|
39
43
|
t.upload
|
40
44
|
when 'export'
|
41
45
|
t = TestImporter.new(options)
|
42
46
|
t.export
|
43
47
|
else
|
44
|
-
logger.fatal
|
48
|
+
logger.fatal 'Unknown command'
|
45
49
|
exit 2
|
46
50
|
end
|
47
51
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
require 'csv'
|
3
|
-
require 'httparty'
|
4
4
|
require 'parallel'
|
5
5
|
require 'ruby-progressbar'
|
6
6
|
|
@@ -28,21 +28,21 @@ module RainforestCli
|
|
28
28
|
columns = rows.shift.map do |column|
|
29
29
|
{name: column.downcase.strip.gsub(/\s/, '_')}
|
30
30
|
end
|
31
|
-
raise 'Invalid schema in CSV. You must include headers in first row.' if
|
31
|
+
raise 'Invalid schema in CSV. You must include headers in first row.' if !columns
|
32
32
|
|
33
|
-
print
|
34
|
-
response = client.post
|
33
|
+
print 'Creating custom step variable'
|
34
|
+
response = client.post '/generators', { name: @generator_name, description: @generator_name, columns: columns }
|
35
35
|
raise "Error creating custom step variable: #{response['error']}" if response['error']
|
36
36
|
puts "\t[OK]"
|
37
37
|
|
38
38
|
@columns = response['columns']
|
39
39
|
@generator_id = response['id']
|
40
40
|
|
41
|
-
puts
|
41
|
+
puts 'Uploading data...'
|
42
42
|
p = ProgressBar.create(title: 'Rows', total: rows.count, format: '%a %B %p%% %t')
|
43
43
|
|
44
44
|
# Insert the data
|
45
|
-
Parallel.each(rows, in_threads: 16, finish: lambda { |
|
45
|
+
Parallel.each(rows, in_threads: 16, finish: lambda { |_item, _i, _result| p.increment }) do |row|
|
46
46
|
response = client.post("/generators/#{@generator_id}/rows", {data: row_data(@columns, row)})
|
47
47
|
raise response['error'] if response['error']
|
48
48
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'optparse'
|
2
3
|
|
3
4
|
module RainforestCli
|
@@ -7,7 +8,7 @@ module RainforestCli
|
|
7
8
|
end
|
8
9
|
|
9
10
|
def self.extract_hashtags(commit_message)
|
10
|
-
commit_message.partition('@rainforest').last.scan(/#([\w_-]+)/).flatten.map {|s| s.gsub('#','') }
|
11
|
+
commit_message.partition('@rainforest').last.scan(/#([\w_-]+)/).flatten.map {|s| s.gsub('#', '') }
|
11
12
|
end
|
12
13
|
|
13
14
|
def self.last_commit_message
|
@@ -1,8 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'httparty'
|
3
|
+
require 'http/exceptions'
|
4
|
+
|
1
5
|
module RainforestCli
|
2
6
|
class HttpClient
|
3
|
-
API_URL = ENV.fetch(
|
7
|
+
API_URL = ENV.fetch('RAINFOREST_API_URL') do
|
4
8
|
'https://app.rainforestqa.com/api/1'
|
5
9
|
end.freeze
|
10
|
+
RETRY_INTERVAL = 10
|
6
11
|
|
7
12
|
def initialize(options)
|
8
13
|
@token = options.fetch(:token)
|
@@ -28,29 +33,61 @@ module RainforestCli
|
|
28
33
|
JSON.parse(response.body)
|
29
34
|
end
|
30
35
|
|
31
|
-
def get(url, body = {})
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
def get(url, body = {}, retries_on_failures: false)
|
37
|
+
wrap_exceptions(retries_on_failures) do
|
38
|
+
response = HTTParty.get make_url(url), {
|
39
|
+
body: body,
|
40
|
+
headers: headers,
|
41
|
+
verify: false
|
42
|
+
}
|
37
43
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
44
|
+
if response.code == 200
|
45
|
+
return JSON.parse(response.body)
|
46
|
+
else
|
47
|
+
return nil
|
48
|
+
end
|
42
49
|
end
|
43
50
|
end
|
44
51
|
|
45
52
|
private
|
53
|
+
def wrap_exceptions(retries_on_failures)
|
54
|
+
@retry_delay = 0
|
55
|
+
@waiting_on_retries = false
|
56
|
+
loop do
|
57
|
+
begin
|
58
|
+
# Suspend tries until wait period is over
|
59
|
+
if @waiting_on_retries
|
60
|
+
Kernel.sleep 5
|
61
|
+
else
|
62
|
+
Http::Exceptions.wrap_exception { yield }
|
63
|
+
break
|
64
|
+
end
|
65
|
+
rescue Http::Exceptions::HttpException, Timeout::Error => e
|
66
|
+
raise e unless retries_on_failures
|
67
|
+
|
68
|
+
unless @waiting_on_retries
|
69
|
+
@waiting_on_retries = true
|
70
|
+
@retry_delay += RETRY_INTERVAL
|
71
|
+
|
72
|
+
RainforestCli.logger.warn 'Exception Encountered while trying to contact Rainforest API:'
|
73
|
+
RainforestCli.logger.warn "\t\t#{e.message}"
|
74
|
+
RainforestCli.logger.warn "Retrying again in #{@retry_delay} seconds..."
|
75
|
+
|
76
|
+
Kernel.sleep @retry_delay
|
77
|
+
@waiting_on_retries = false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
46
83
|
def make_url(url)
|
47
84
|
File.join(API_URL, url)
|
48
85
|
end
|
49
86
|
|
50
87
|
def headers
|
51
88
|
{
|
52
|
-
|
53
|
-
|
89
|
+
'CLIENT_TOKEN' => @token,
|
90
|
+
'User-Agent' => "Rainforest-cli-#{RainforestCli::VERSION}"
|
54
91
|
}
|
55
92
|
end
|
56
93
|
end
|
@@ -1,13 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'optparse'
|
2
3
|
|
3
4
|
module RainforestCli
|
4
|
-
class BrowserException < Exception
|
5
|
-
def initialize browsers
|
6
|
-
invalid_browsers = browsers - OptionParser::VALID_BROWSERS
|
7
|
-
super "#{invalid_browsers.join(', ')} is not valid. Valid browsers: #{OptionParser::VALID_BROWSERS.join(', ')}"
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
5
|
class OptionParser
|
12
6
|
attr_writer :file_name, :tags
|
13
7
|
attr_reader :command, :token, :tags, :conflict, :browsers, :site_id, :environment_id,
|
@@ -16,92 +10,123 @@ module RainforestCli
|
|
16
10
|
|
17
11
|
# Note, not all of these may be available to your account
|
18
12
|
# also, we may remove this in the future.
|
19
|
-
VALID_BROWSERS = %w{
|
13
|
+
VALID_BROWSERS = %w{
|
14
|
+
android_phone_landscape
|
15
|
+
android_phone_portrait
|
16
|
+
android_tablet_landscape
|
17
|
+
android_tablet_portrait
|
18
|
+
chrome
|
19
|
+
chrome_1440_900
|
20
|
+
chrome_adblock
|
21
|
+
chrome_ghostery
|
22
|
+
chrome_guru
|
23
|
+
chrome_ublock
|
24
|
+
firefox
|
25
|
+
firefox_1440_900
|
26
|
+
ie10
|
27
|
+
ie10_1440_900
|
28
|
+
ie11
|
29
|
+
ie11_1440_900
|
30
|
+
ie8
|
31
|
+
ie8_1440_900
|
32
|
+
ie9
|
33
|
+
ie9_1440_900
|
34
|
+
ios_iphone4s
|
35
|
+
office2010
|
36
|
+
office2013
|
37
|
+
osx_chrome
|
38
|
+
osx_firefox
|
39
|
+
safari
|
40
|
+
ubuntu_chrome
|
41
|
+
ubuntu_firefox
|
42
|
+
}.freeze
|
43
|
+
TOKEN_NOT_REQUIRED = %w{new validate}.freeze
|
20
44
|
|
21
45
|
def initialize(args)
|
22
46
|
@args = args.dup
|
23
47
|
@tags = []
|
24
48
|
@browsers = nil
|
25
|
-
@require_token = true
|
26
49
|
@debug = false
|
27
50
|
|
51
|
+
# NOTE: Disabling line length cop to allow for consistency of syntax
|
52
|
+
# rubocop:disable Metrics/LineLength
|
28
53
|
@parsed = ::OptionParser.new do |opts|
|
29
|
-
opts.on(
|
54
|
+
opts.on('--debug') do
|
30
55
|
@debug = true
|
31
56
|
end
|
32
57
|
|
33
|
-
opts.on(
|
58
|
+
opts.on('--file') do |value|
|
34
59
|
@file_name = value
|
35
60
|
end
|
36
61
|
|
37
|
-
opts.on(
|
62
|
+
opts.on('--test-folder FILE_PATH', 'Specify the test folder. Defaults to spec/rainforest if not set.') do |value|
|
38
63
|
@test_folder = value
|
39
64
|
end
|
40
65
|
|
41
|
-
opts.on(
|
66
|
+
opts.on('--import-variable-csv-file FILE', 'Import step variables; CSV data') do |value|
|
42
67
|
@import_file_name = value
|
43
68
|
end
|
44
69
|
|
45
|
-
opts.on(
|
70
|
+
opts.on('--import-variable-name NAME', 'Import step variables; Name of variable (note, will be replaced if exists)') do |value|
|
46
71
|
@import_name = value
|
47
72
|
end
|
48
73
|
|
49
|
-
opts.on(
|
74
|
+
opts.on('--git-trigger', 'Only run if the last commit contains @rainforestapp') do |_value|
|
50
75
|
@git_trigger = true
|
51
76
|
end
|
52
77
|
|
53
|
-
opts.on(
|
78
|
+
opts.on('--fg', 'Run the tests in foreground.') do |value|
|
54
79
|
@foreground = value
|
55
80
|
end
|
56
81
|
|
57
|
-
opts.on(
|
82
|
+
opts.on('--fail-fast', String, "Fail as soon as there is a failure (don't wait for completion)") do |_value|
|
58
83
|
@failfast = true
|
59
84
|
end
|
60
85
|
|
61
|
-
opts.on(
|
86
|
+
opts.on('--token API_TOKEN', String, 'Your rainforest API token.') do |value|
|
62
87
|
@token = value
|
63
88
|
end
|
64
89
|
|
65
|
-
opts.on(
|
90
|
+
opts.on('--tag TAG', String, 'A tag to run the tests with') do |value|
|
66
91
|
@tags << value
|
67
92
|
end
|
68
93
|
|
69
|
-
opts.on(
|
94
|
+
opts.on('--folder ID', 'Run tests in the specified folders') do |value|
|
70
95
|
@folder = value
|
71
96
|
end
|
72
97
|
|
73
|
-
opts.on(
|
74
|
-
@browsers = value.split(',').map{|x| x.strip.downcase }.uniq
|
75
|
-
|
76
|
-
raise BrowserException, @browsers unless (@browsers - VALID_BROWSERS).empty?
|
98
|
+
opts.on('--browsers LIST', 'Run against the specified browsers') do |value|
|
99
|
+
@browsers = value.split(',').map {|x| x.strip.downcase }.uniq
|
77
100
|
end
|
78
101
|
|
79
|
-
opts.on(
|
102
|
+
opts.on('--conflict MODE', String, 'How should Rainforest handle existing in progress runs?') do |value|
|
80
103
|
@conflict = value
|
81
104
|
end
|
82
105
|
|
83
|
-
opts.on(
|
106
|
+
opts.on('--environment-id ID', Integer, 'Run using this environment. If excluded, will use your default') do |value|
|
84
107
|
@environment_id = value
|
85
108
|
end
|
86
109
|
|
87
|
-
opts.on(
|
110
|
+
opts.on('--site-id ID', Integer, 'Only run tests for a specific site') do |value|
|
88
111
|
@site_id = value
|
89
112
|
end
|
90
113
|
|
91
|
-
opts.on(
|
114
|
+
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|
|
92
115
|
@custom_url = value
|
93
116
|
end
|
94
117
|
|
95
|
-
opts.on(
|
118
|
+
opts.on('--description DESCRIPTION', 'Add a description for the run.') do |value|
|
96
119
|
@description = value
|
97
120
|
end
|
98
121
|
|
99
|
-
opts.on_tail(
|
122
|
+
opts.on_tail('--help', 'Display help message and exit') do |_value|
|
100
123
|
puts opts
|
101
124
|
exit 0
|
102
125
|
end
|
103
126
|
|
104
127
|
end.parse!(@args)
|
128
|
+
# rubocop:enable Metrics/LineLength
|
129
|
+
# NOTE: end Rubocop exception
|
105
130
|
|
106
131
|
@command = @args.shift
|
107
132
|
|
@@ -110,10 +135,6 @@ module RainforestCli
|
|
110
135
|
end
|
111
136
|
|
112
137
|
@tests = @args.dup
|
113
|
-
|
114
|
-
# A couple of commands don't need the token
|
115
|
-
token_not_required = %w{new validate}
|
116
|
-
@require_token = false if token_not_required.include?(@command)
|
117
138
|
end
|
118
139
|
|
119
140
|
def tests
|
@@ -133,28 +154,39 @@ module RainforestCli
|
|
133
154
|
end
|
134
155
|
|
135
156
|
def validate!
|
136
|
-
if
|
157
|
+
if !TOKEN_NOT_REQUIRED.include?(command)
|
137
158
|
unless token
|
138
|
-
raise ValidationError,
|
159
|
+
raise ValidationError, 'You must pass your API token using: --token TOKEN'
|
139
160
|
end
|
140
161
|
end
|
141
162
|
|
163
|
+
if browsers
|
164
|
+
raise BrowserException, browsers unless (browsers - VALID_BROWSERS).empty?
|
165
|
+
end
|
166
|
+
|
142
167
|
if custom_url && site_id.nil?
|
143
|
-
raise ValidationError,
|
168
|
+
raise ValidationError, 'The site-id and custom-url options are both required.'
|
144
169
|
end
|
145
170
|
|
146
171
|
if import_file_name && import_name
|
147
|
-
unless File.
|
172
|
+
unless File.exist?(import_file_name)
|
148
173
|
raise ValidationError, "Input file: #{import_file_name} not found"
|
149
174
|
end
|
150
175
|
|
151
176
|
elsif import_file_name || import_name
|
152
|
-
raise ValidationError,
|
177
|
+
raise ValidationError, 'You must pass both --import-variable-csv-file and --import-variable-name'
|
153
178
|
end
|
154
179
|
true
|
155
180
|
end
|
156
181
|
|
157
182
|
class ValidationError < RuntimeError
|
158
183
|
end
|
184
|
+
|
185
|
+
class BrowserException < ValidationError
|
186
|
+
def initialize browsers
|
187
|
+
invalid_browsers = browsers - OptionParser::VALID_BROWSERS
|
188
|
+
super "#{invalid_browsers.join(', ')} is not valid. Valid browsers: #{OptionParser::VALID_BROWSERS.join(', ')}"
|
189
|
+
end
|
190
|
+
end
|
159
191
|
end
|
160
192
|
end
|