rainforest-cli 1.0.6 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|