rainforest-cli 1.0.6 → 1.1.0
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 +1 -0
- data/.rvmrc +1 -1
- data/.travis.yml +2 -0
- data/CHANGELOG.md +3 -0
- data/README.md +2 -1
- data/bin/rainforest +1 -1
- data/lib/rainforest/cli.rb +40 -18
- data/lib/rainforest/cli/csv_importer.rb +37 -39
- data/lib/rainforest/cli/git_trigger.rb +10 -12
- data/lib/rainforest/cli/http_client.rb +46 -45
- data/lib/rainforest/cli/options.rb +113 -92
- data/lib/rainforest/cli/runner.rb +116 -119
- data/lib/rainforest/cli/test_importer.rb +238 -0
- data/lib/rainforest/cli/test_parser.rb +100 -0
- data/lib/rainforest/cli/version.rb +2 -4
- data/rainforest-cli.gemspec +2 -1
- data/spec/cli_spec.rb +5 -5
- data/spec/csv_importer_spec.rb +2 -2
- data/spec/git_trigger_spec.rb +1 -1
- data/spec/options_spec.rb +12 -5
- data/spec/runner_spec.rb +2 -2
- data/spec/spec_helper.rb +1 -1
- metadata +19 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 93bba481d733b42bc5f7f7fb501e931264c6e781
|
4
|
+
data.tar.gz: d4892987ecabb73e6fd9063e409052b54ac849e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e2b9375fef282917fd66997b4a11715740ea66de6420b8c6e8d1af452fe96cc09a59258bf363a3fda725024a70cc43952c29c51aaf4450d624a10581f91acbd
|
7
|
+
data.tar.gz: f829dfa712a368139bd81590fd47022f05b515bc7f3bb2aa4224854f3f1828b42f0f57027af21081dbe34a387a2691fd1d549426d2289e71871c2c6028c6d975
|
data/.gitignore
CHANGED
data/.rvmrc
CHANGED
@@ -1 +1 @@
|
|
1
|
-
rvm use 2.
|
1
|
+
rvm use 2.3.0@rainforest-cli --create
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -56,7 +56,8 @@ The most popular options are:
|
|
56
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.
|
57
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))!)
|
58
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.
|
59
|
-
- `--
|
59
|
+
- `--environment-id` - run your tests using this environment. Otherwise it will use your default environment
|
60
|
+
- `--conflict OPTION` - use the `abort` option to abort any runs in progress in the same environment as your new run. use the `abort-all` option to abort all runs in progress.
|
60
61
|
- `--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
62
|
- `--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
63
|
- `--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.
|
data/bin/rainforest
CHANGED
data/lib/rainforest/cli.rb
CHANGED
@@ -4,36 +4,58 @@ require "rainforest/cli/runner"
|
|
4
4
|
require "rainforest/cli/http_client"
|
5
5
|
require "rainforest/cli/git_trigger"
|
6
6
|
require "rainforest/cli/csv_importer"
|
7
|
+
require "rainforest/cli/test_importer"
|
8
|
+
require "rainforest/cli/test_parser"
|
7
9
|
require "erb"
|
8
10
|
require "httparty"
|
9
11
|
require "json"
|
10
12
|
require "logger"
|
11
13
|
|
12
|
-
module
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
OptionParser.new(['--help']) if args.size == 0
|
14
|
+
module RainforestCli
|
15
|
+
def self.start(args)
|
16
|
+
options = OptionParser.new(args)
|
17
|
+
OptionParser.new(['--help']) if args.size == 0
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
begin
|
20
|
+
options.validate!
|
21
|
+
rescue OptionParser::ValidationError => e
|
22
|
+
logger.fatal e.message
|
23
|
+
exit 2
|
24
|
+
end
|
24
25
|
|
26
|
+
case options.command
|
27
|
+
when 'run'
|
25
28
|
runner = Runner.new(options)
|
26
29
|
runner.run
|
30
|
+
when 'new'
|
31
|
+
t = TestImporter.new(options)
|
32
|
+
t.create_new
|
33
|
+
when 'validate'
|
34
|
+
t = TestImporter.new(options)
|
35
|
+
t.validate
|
36
|
+
when 'upload'
|
37
|
+
t = TestImporter.new(options)
|
38
|
+
t.upload
|
27
39
|
|
28
|
-
|
40
|
+
# options.tags = ['ro']
|
41
|
+
# runner = Runner.new(options)
|
42
|
+
# runner.run
|
43
|
+
when 'export'
|
44
|
+
t = TestImporter.new(options)
|
45
|
+
t.export
|
46
|
+
else
|
47
|
+
logger.fatal "Unknown command"
|
48
|
+
exit 2
|
29
49
|
end
|
30
50
|
|
31
|
-
|
32
|
-
|
33
|
-
end
|
51
|
+
true
|
52
|
+
end
|
34
53
|
|
35
|
-
|
36
|
-
|
37
|
-
|
54
|
+
def self.logger
|
55
|
+
@logger ||= Logger.new(STDOUT)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.logger=(logger)
|
59
|
+
@logger = logger
|
38
60
|
end
|
39
61
|
end
|
@@ -4,49 +4,47 @@ require 'httparty'
|
|
4
4
|
require 'parallel'
|
5
5
|
require 'ruby-progressbar'
|
6
6
|
|
7
|
-
module
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
7
|
+
module RainforestCli
|
8
|
+
class CSVImporter
|
9
|
+
attr_reader :client
|
10
|
+
|
11
|
+
def initialize name, file, token
|
12
|
+
@generator_name = name
|
13
|
+
@file = file
|
14
|
+
@client = HttpClient.new token: token
|
15
|
+
end
|
16
|
+
|
17
|
+
def row_data columns, values
|
18
|
+
Hash[(columns.map {|c| c['id']}).zip(values)]
|
19
|
+
end
|
20
|
+
|
21
|
+
def import
|
22
|
+
rows = []
|
23
|
+
CSV.foreach(@file, encoding: 'windows-1251:utf-8') do |row|
|
24
|
+
rows << row
|
16
25
|
end
|
17
26
|
|
18
|
-
|
19
|
-
|
27
|
+
# Create the generator
|
28
|
+
columns = rows.shift.map do |column|
|
29
|
+
{name: column.downcase.strip.gsub(/\s/, '_')}
|
20
30
|
end
|
31
|
+
raise 'Invalid schema in CSV. You must include headers in first row.' if not columns
|
32
|
+
|
33
|
+
print "Creating custom step variable"
|
34
|
+
response = client.post "/generators", { name: @generator_name, description: @generator_name, columns: columns }
|
35
|
+
raise "Error creating custom step variable: #{response['error']}" if response['error']
|
36
|
+
puts "\t[OK]"
|
37
|
+
|
38
|
+
@columns = response['columns']
|
39
|
+
@generator_id = response['id']
|
40
|
+
|
41
|
+
puts "Uploading data..."
|
42
|
+
p = ProgressBar.create(title: 'Rows', total: rows.count, format: '%a %B %p%% %t')
|
21
43
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
end
|
27
|
-
|
28
|
-
# Create the generator
|
29
|
-
columns = rows.shift.map do |column|
|
30
|
-
{name: column.downcase.strip.gsub(/\s/, '_')}
|
31
|
-
end
|
32
|
-
raise 'Invalid schema in CSV. You must include headers in first row.' if not columns
|
33
|
-
|
34
|
-
print "Creating custom step variable"
|
35
|
-
response = client.post "/generators", { name: @generator_name, description: @generator_name, columns: columns }
|
36
|
-
raise "Error creating custom step variable: #{response['error']}" if response['error']
|
37
|
-
puts "\t[OK]"
|
38
|
-
|
39
|
-
@columns = response['columns']
|
40
|
-
@generator_id = response['id']
|
41
|
-
|
42
|
-
puts "Uploading data..."
|
43
|
-
p = ProgressBar.create(title: 'Rows', total: rows.count, format: '%a %B %p%% %t')
|
44
|
-
|
45
|
-
# Insert the data
|
46
|
-
Parallel.each(rows, in_threads: 16, finish: lambda { |item, i, result| p.increment }) do |row|
|
47
|
-
response = client.post("/generators/#{@generator_id}/rows", {data: row_data(@columns, row)})
|
48
|
-
raise response['error'] if response['error']
|
49
|
-
end
|
44
|
+
# Insert the data
|
45
|
+
Parallel.each(rows, in_threads: 16, finish: lambda { |item, i, result| p.increment }) do |row|
|
46
|
+
response = client.post("/generators/#{@generator_id}/rows", {data: row_data(@columns, row)})
|
47
|
+
raise response['error'] if response['error']
|
50
48
|
end
|
51
49
|
end
|
52
50
|
end
|
@@ -1,19 +1,17 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
3
|
+
module RainforestCli
|
4
|
+
class GitTrigger
|
5
|
+
def self.git_trigger_should_run?(commit_message)
|
6
|
+
commit_message.include?('@rainforest')
|
7
|
+
end
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
9
|
+
def self.extract_hashtags(commit_message)
|
10
|
+
commit_message.scan(/#([\w_-]+)/).flatten.map {|s| s.gsub('#','') }
|
11
|
+
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
end
|
13
|
+
def self.last_commit_message
|
14
|
+
`git log -1 --pretty=%B`.strip
|
17
15
|
end
|
18
16
|
end
|
19
17
|
end
|
@@ -1,56 +1,57 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
1
|
+
module RainforestCli
|
2
|
+
class HttpClient
|
3
|
+
API_URL = ENV.fetch("RAINFOREST_API_URL") do
|
4
|
+
'https://app.rainforestqa.com/api/1'
|
5
|
+
end.freeze
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@token = options.fetch(:token)
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
def delete(url, body = {})
|
12
|
+
response = HTTParty.delete make_url(url), {
|
13
|
+
body: body,
|
14
|
+
headers: headers,
|
15
|
+
verify: false
|
16
|
+
}
|
17
17
|
|
18
|
-
|
19
|
-
|
18
|
+
JSON.parse(response.body)
|
19
|
+
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
def post(url, body = {})
|
22
|
+
response = HTTParty.post make_url(url), {
|
23
|
+
body: body,
|
24
|
+
headers: headers,
|
25
|
+
verify: false
|
26
|
+
}
|
26
27
|
|
27
|
-
|
28
|
-
|
28
|
+
JSON.parse(response.body)
|
29
|
+
end
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
if response.code == 200
|
37
|
-
JSON.parse(response.body)
|
38
|
-
else
|
39
|
-
nil
|
40
|
-
end
|
41
|
-
end
|
31
|
+
def get(url, body = {})
|
32
|
+
response = HTTParty.get make_url(url), {
|
33
|
+
body: body,
|
34
|
+
headers: headers,
|
35
|
+
verify: false
|
36
|
+
}
|
42
37
|
|
43
|
-
|
44
|
-
|
45
|
-
|
38
|
+
if response.code == 200
|
39
|
+
JSON.parse(response.body)
|
40
|
+
else
|
41
|
+
nil
|
46
42
|
end
|
43
|
+
end
|
47
44
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
45
|
+
private
|
46
|
+
def make_url(url)
|
47
|
+
File.join(API_URL, url)
|
48
|
+
end
|
49
|
+
|
50
|
+
def headers
|
51
|
+
{
|
52
|
+
"CLIENT_TOKEN" => @token,
|
53
|
+
"User-Agent" => "Rainforest-cli-#{RainforestCli::VERSION}"
|
54
|
+
}
|
54
55
|
end
|
55
56
|
end
|
56
57
|
end
|
@@ -1,133 +1,154 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
super "#{invalid_browsers.join(', ')} is not valid. Valid browsers: #{OptionParser::VALID_BROWSERS.join(', ')}"
|
9
|
-
end
|
3
|
+
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(', ')}"
|
10
8
|
end
|
9
|
+
end
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
class OptionParser
|
12
|
+
attr_writer :file_name, :tags
|
13
|
+
attr_reader :command, :token, :tags, :conflict, :browsers, :site_id, :environment_id,
|
14
|
+
:import_file_name, :import_name, :custom_url, :description, :folder,
|
15
|
+
:debug, :file_name
|
15
16
|
|
16
|
-
|
17
|
+
VALID_BROWSERS = %w{chrome firefox safari ie8 ie9}.freeze
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
def initialize(args)
|
20
|
+
@args = args.dup
|
21
|
+
@tags = []
|
22
|
+
@browsers = nil
|
23
|
+
@require_token = true
|
24
|
+
@debug = false
|
22
25
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
@parsed = ::OptionParser.new do |opts|
|
27
|
+
opts.on("--debug") do
|
28
|
+
@debug = true
|
29
|
+
end
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
+
opts.on("--file") do |value|
|
32
|
+
@file_name = value
|
33
|
+
end
|
31
34
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
+
opts.on("--import-variable-csv-file FILE", "Import step variables; CSV data") do |value|
|
36
|
+
@import_file_name = value
|
37
|
+
end
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
opts.on("--import-variable-name NAME", "Import step variables; Name of variable (note, will be replaced if exists)") do |value|
|
40
|
+
@import_name = value
|
41
|
+
end
|
39
42
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
+
opts.on("--git-trigger", "Only run if the last commit contains @rainforestapp") do |value|
|
44
|
+
@git_trigger = true
|
45
|
+
end
|
43
46
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
+
opts.on("--fg", "Run the tests in foreground.") do |value|
|
48
|
+
@foreground = value
|
49
|
+
end
|
47
50
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
+
opts.on("--fail-fast", String, "Fail as soon as there is a failure (don't wait for completion)") do |value|
|
52
|
+
@failfast = true
|
53
|
+
end
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
+
opts.on("--token TOKEN", String, "Your rainforest API token.") do |value|
|
56
|
+
@token = value
|
57
|
+
end
|
55
58
|
|
56
|
-
|
57
|
-
|
59
|
+
opts.on("--tag TAG", String, "A tag to run the tests with") do |value|
|
60
|
+
@tags << value
|
61
|
+
end
|
58
62
|
|
59
|
-
|
60
|
-
|
63
|
+
opts.on("--folder ID", "Run tests in the specified folders") do |value|
|
64
|
+
@folder = value
|
65
|
+
end
|
61
66
|
|
62
|
-
|
63
|
-
|
64
|
-
end
|
67
|
+
opts.on("--browsers LIST", "Run against the specified browsers") do |value|
|
68
|
+
@browsers = value.split(',').map{|x| x.strip.downcase }.uniq
|
65
69
|
|
66
|
-
|
67
|
-
|
68
|
-
end
|
70
|
+
raise BrowserException, @browsers unless (@browsers - VALID_BROWSERS).empty?
|
71
|
+
end
|
69
72
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
+
opts.on("--conflict MODE", String, "How should Rainforest handle existing in progress runs?") do |value|
|
74
|
+
@conflict = value
|
75
|
+
end
|
73
76
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
+
opts.on("--environment-id ID", Integer, "Run using this environment. If excluded, will use your default") do |value|
|
78
|
+
@environment_id = value
|
79
|
+
end
|
77
80
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
+
opts.on("--site-id ID", Integer, "Only run tests for a specific site") do |value|
|
82
|
+
@site_id = value
|
83
|
+
end
|
81
84
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
85
|
+
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|
|
86
|
+
@custom_url = value
|
87
|
+
end
|
86
88
|
|
87
|
-
|
89
|
+
opts.on("--description DESCRIPTION", "Add a description for the run.") do |value|
|
90
|
+
@description = value
|
91
|
+
end
|
88
92
|
|
89
|
-
|
90
|
-
|
91
|
-
|
93
|
+
opts.on_tail("--help", "Display help message and exit") do |value|
|
94
|
+
puts opts
|
95
|
+
exit 0
|
96
|
+
end
|
92
97
|
|
93
|
-
|
94
|
-
@tests
|
95
|
-
end
|
98
|
+
end.parse!(@args)
|
96
99
|
|
97
|
-
|
98
|
-
@git_trigger
|
99
|
-
end
|
100
|
+
@command = @args.shift
|
100
101
|
|
101
|
-
|
102
|
-
@
|
102
|
+
if @command == 'new'
|
103
|
+
@file_name = @args.shift
|
103
104
|
end
|
104
105
|
|
105
|
-
|
106
|
-
|
107
|
-
|
106
|
+
@tests = @args.dup
|
107
|
+
|
108
|
+
# A couple of commands don't need the token
|
109
|
+
token_not_required = %w{new validate}
|
110
|
+
@require_token = false if token_not_required.include?(@command)
|
111
|
+
end
|
112
|
+
|
113
|
+
def tests
|
114
|
+
@tests
|
115
|
+
end
|
116
|
+
|
117
|
+
def git_trigger?
|
118
|
+
@git_trigger
|
119
|
+
end
|
120
|
+
|
121
|
+
def failfast?
|
122
|
+
@failfast
|
123
|
+
end
|
124
|
+
|
125
|
+
def foreground?
|
126
|
+
@foreground
|
127
|
+
end
|
108
128
|
|
109
|
-
|
129
|
+
def validate!
|
130
|
+
if @require_token
|
110
131
|
unless token
|
111
132
|
raise ValidationError, "You must pass your API token using: --token TOKEN"
|
112
133
|
end
|
134
|
+
end
|
113
135
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
if import_file_name && import_name
|
119
|
-
unless File.exists?(import_file_name)
|
120
|
-
raise ValidationError, "Input file: #{import_file_name} not found"
|
121
|
-
end
|
136
|
+
if custom_url && site_id.nil?
|
137
|
+
raise ValidationError, "The site-id and custom-url options are both required."
|
138
|
+
end
|
122
139
|
|
123
|
-
|
124
|
-
|
140
|
+
if import_file_name && import_name
|
141
|
+
unless File.exists?(import_file_name)
|
142
|
+
raise ValidationError, "Input file: #{import_file_name} not found"
|
125
143
|
end
|
126
|
-
true
|
127
|
-
end
|
128
144
|
|
129
|
-
|
145
|
+
elsif import_file_name || import_name
|
146
|
+
raise ValidationError, "You must pass both --import-variable-csv-file and --import-variable-name"
|
130
147
|
end
|
148
|
+
true
|
149
|
+
end
|
150
|
+
|
151
|
+
class ValidationError < RuntimeError
|
131
152
|
end
|
132
153
|
end
|
133
154
|
end
|