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.
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