rainforest-cli 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/rainforestapp/rainforest-cli.png?branch=master)](https://travis-ci.org/rainforestapp/rainforest-cli)
|
2
2
|
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/rainforest-cli.svg)](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
|