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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +5 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +8 -0
  7. data/Gemfile +7 -1
  8. data/README.md +2 -0
  9. data/Rakefile +6 -4
  10. data/circle.yml +3 -0
  11. data/lib/rainforest/cli.rb +20 -16
  12. data/lib/rainforest/cli/constants.rb +4 -0
  13. data/lib/rainforest/cli/csv_importer.rb +6 -6
  14. data/lib/rainforest/cli/git_trigger.rb +2 -1
  15. data/lib/rainforest/cli/http_client.rb +50 -13
  16. data/lib/rainforest/cli/options.rb +71 -39
  17. data/lib/rainforest/cli/remote_tests.rb +49 -0
  18. data/lib/rainforest/cli/runner.rb +19 -17
  19. data/lib/rainforest/cli/test_files.rb +32 -14
  20. data/lib/rainforest/cli/test_importer.rb +35 -155
  21. data/lib/rainforest/cli/test_parser.rb +38 -14
  22. data/lib/rainforest/cli/uploader.rb +107 -0
  23. data/lib/rainforest/cli/validator.rb +158 -0
  24. data/lib/rainforest/cli/version.rb +2 -1
  25. data/rainforest-cli.gemspec +14 -12
  26. data/spec/cli_spec.rb +84 -90
  27. data/spec/csv_importer_spec.rb +13 -8
  28. data/spec/git_trigger_spec.rb +28 -15
  29. data/spec/http_client_spec.rb +57 -0
  30. data/spec/options_spec.rb +72 -70
  31. data/spec/rainforest-example/example_test.rfml +2 -1
  32. data/spec/remote_tests_spec.rb +22 -0
  33. data/spec/runner_spec.rb +17 -16
  34. data/spec/spec_helper.rb +16 -9
  35. data/spec/test_files_spec.rb +20 -24
  36. data/spec/uploader_spec.rb +54 -0
  37. data/spec/validation-examples/circular_embeds/test1.rfml +5 -0
  38. data/spec/validation-examples/circular_embeds/test2.rfml +5 -0
  39. data/spec/validation-examples/correct_embeds/embedded_test.rfml +6 -0
  40. data/spec/validation-examples/correct_embeds/test_with_embedded.rfml +8 -0
  41. data/spec/validation-examples/missing_embeds/correct_test.rfml +8 -0
  42. data/spec/validation-examples/missing_embeds/incorrect_test.rfml +8 -0
  43. data/spec/validation-examples/parse_errors/no_parse_errors.rfml +6 -0
  44. data/spec/validation-examples/parse_errors/no_question.rfml +5 -0
  45. data/spec/validation-examples/parse_errors/no_question_mark.rfml +6 -0
  46. data/spec/validation-examples/parse_errors/no_rfml_id.rfml +5 -0
  47. data/spec/validator_spec.rb +119 -0
  48. metadata +96 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5a4d51888c0d3ce1b8845532734aa59145159b2f
4
- data.tar.gz: 49d5541c943fc62aca7dbd5c173823cf6c64743c
3
+ metadata.gz: 04fa2b25294be20d03d2c6f41b74d19d806b0fe5
4
+ data.tar.gz: 5fe743cb8e0fae9c345c191d1e01f73705e3c24a
5
5
  SHA512:
6
- metadata.gz: 8f8479a400d1d88eaf84e0eeb584a1e6a8e56590e89b628cfbd9cb8c0126cc4b6b0e64c4ff9c8427f31d6f79795ad2a43abd5b27c03a10232fdc3481dd77c1a4
7
- data.tar.gz: 1e1337f1e45e4b5131e612d0bea3fef8d01686ea043af10d7deb2d5d76baf79a13d6a558773619ace488f8f7acbb01ae5b3090d5aeac968f5c9e54494e610888
6
+ metadata.gz: 3bc25ec6f023a5d1078538d7368552a970a0079652bb04bfd20c9f5e9237b2d707a3ae88eb51ed82df87ed3188b8d921464adb0dcaf6a23f663b510156a75d2d
7
+ data.tar.gz: 510f8eff35dddb1f889e3afa2db044fc393db312d89bcbf22ea1db330bf1a03286fb9bdc3c8baf2280aba0bb7d4d4a9f4a44d6a99161ce92aae91161887045ae
data/.gitignore CHANGED
@@ -16,6 +16,8 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  tags
19
+ .byebug_history
19
20
 
20
21
  *.csv
21
22
  spec/rainforest
23
+ spec/rainforest-testing
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
2
  --require spec_helper
3
+ --format documentation
data/.rubocop.yml ADDED
@@ -0,0 +1,5 @@
1
+ inherit_gem:
2
+ rf-stylez: ruby/rubocop.yml
3
+
4
+ Style/FileName:
5
+ Exclude: [rainforest-cli.gemspec]
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 "rspec", "2.14"
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
- require "bundler/gem_tasks"
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 :default => :spec
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
@@ -0,0 +1,3 @@
1
+ test:
2
+ pre:
3
+ - bundle exec circlemator style-check --base-branch=master
@@ -1,16 +1,20 @@
1
- require "rainforest/cli/version"
2
- require "rainforest/cli/options"
3
- require "rainforest/cli/runner"
4
- require "rainforest/cli/http_client"
5
- require "rainforest/cli/git_trigger"
6
- require "rainforest/cli/csv_importer"
7
- require "rainforest/cli/test_parser"
8
- require "rainforest/cli/test_files"
9
- require "rainforest/cli/test_importer"
10
- require "erb"
11
- require "httparty"
12
- require "json"
13
- require "logger"
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 = TestImporter.new(options)
39
+ t = Validator.new(options)
36
40
  t.validate
37
41
  when 'upload'
38
- t = TestImporter.new(options)
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 "Unknown command"
48
+ logger.fatal 'Unknown command'
45
49
  exit 2
46
50
  end
47
51
 
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module RainforestCli
3
+ THREADS = 32
4
+ end
@@ -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 not columns
31
+ raise 'Invalid schema in CSV. You must include headers in first row.' if !columns
32
32
 
33
- print "Creating custom step variable"
34
- response = client.post "/generators", { name: @generator_name, description: @generator_name, columns: columns }
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 "Uploading data..."
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 { |item, i, result| p.increment }) do |row|
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("RAINFOREST_API_URL") do
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
- response = HTTParty.get make_url(url), {
33
- body: body,
34
- headers: headers,
35
- verify: false
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
- if response.code == 200
39
- JSON.parse(response.body)
40
- else
41
- nil
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
- "CLIENT_TOKEN" => @token,
53
- "User-Agent" => "Rainforest-cli-#{RainforestCli::VERSION}"
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{android_phone_landscape android_phone_portrait android_tablet_landscape android_tablet_portrait chrome chrome_1440_900 chrome_adblock chrome_ghostery chrome_guru chrome_ublock firefox firefox_1440_900 ie10 ie10_1440_900 ie11 ie11_1440_900 ie8 ie8_1440_900 ie9 ie9_1440_900 ios_iphone4s office2010 office2013 osx_chrome osx_firefox safari ubuntu_chrome ubuntu_firefox}.freeze
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("--debug") do
54
+ opts.on('--debug') do
30
55
  @debug = true
31
56
  end
32
57
 
33
- opts.on("--file") do |value|
58
+ opts.on('--file') do |value|
34
59
  @file_name = value
35
60
  end
36
61
 
37
- opts.on("--test-folder spec/rainforest", "Specify the test folder. Defaults to spec/rainforest if not set.") do |value|
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("--import-variable-csv-file FILE", "Import step variables; CSV data") do |value|
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("--import-variable-name NAME", "Import step variables; Name of variable (note, will be replaced if exists)") do |value|
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("--git-trigger", "Only run if the last commit contains @rainforestapp") do |value|
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("--fg", "Run the tests in foreground.") do |value|
78
+ opts.on('--fg', 'Run the tests in foreground.') do |value|
54
79
  @foreground = value
55
80
  end
56
81
 
57
- opts.on("--fail-fast", String, "Fail as soon as there is a failure (don't wait for completion)") do |value|
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("--token TOKEN", String, "Your rainforest API token.") do |value|
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("--tag TAG", String, "A tag to run the tests with") do |value|
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("--folder ID", "Run tests in the specified folders") do |value|
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("--browsers LIST", "Run against the specified browsers") do |value|
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("--conflict MODE", String, "How should Rainforest handle existing in progress runs?") do |value|
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("--environment-id ID", Integer, "Run using this environment. If excluded, will use your default") do |value|
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("--site-id ID", Integer, "Only run tests for a specific site") do |value|
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("--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|
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("--description DESCRIPTION", "Add a description for the run.") do |value|
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("--help", "Display help message and exit") do |value|
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 @require_token
157
+ if !TOKEN_NOT_REQUIRED.include?(command)
137
158
  unless token
138
- raise ValidationError, "You must pass your API token using: --token TOKEN"
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, "The site-id and custom-url options are both required."
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.exists?(import_file_name)
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, "You must pass both --import-variable-csv-file and --import-variable-name"
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