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
@@ -1,156 +1,153 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
attr_reader :options, :client
|
5
|
-
|
6
|
-
def initialize(options)
|
7
|
-
@options = options
|
8
|
-
@client = HttpClient.new token: options.token
|
9
|
-
end
|
1
|
+
module RainforestCli
|
2
|
+
class Runner
|
3
|
+
attr_reader :options, :client
|
10
4
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
@client = HttpClient.new token: options.token
|
8
|
+
end
|
16
9
|
|
17
|
-
|
10
|
+
def run
|
11
|
+
if options.import_file_name && options.import_name
|
12
|
+
delete_generator(options.import_name)
|
13
|
+
CSVImporter.new(options.import_name, options.import_file_name, options.token).import
|
14
|
+
end
|
18
15
|
|
19
|
-
|
20
|
-
logger.info "Issuing run"
|
16
|
+
post_opts = make_create_run_options
|
21
17
|
|
22
|
-
|
18
|
+
logger.debug "POST options: #{post_opts.inspect}"
|
19
|
+
logger.info "Issuing run"
|
23
20
|
|
24
|
-
|
25
|
-
logger.fatal "Error starting your run: #{response['error']}"
|
26
|
-
exit 1
|
27
|
-
end
|
21
|
+
response = client.post('/runs', post_opts)
|
28
22
|
|
23
|
+
if response['error']
|
24
|
+
logger.fatal "Error starting your run: #{response['error']}"
|
25
|
+
exit 1
|
26
|
+
end
|
29
27
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
28
|
+
if options.foreground?
|
29
|
+
run_id = response.fetch("id")
|
30
|
+
wait_for_run_completion(run_id)
|
31
|
+
else
|
32
|
+
true
|
36
33
|
end
|
34
|
+
end
|
37
35
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
end
|
36
|
+
def wait_for_run_completion(run_id)
|
37
|
+
running = true
|
38
|
+
while running
|
39
|
+
Kernel.sleep 5
|
40
|
+
response = client.get("/runs/#{run_id}")
|
41
|
+
if response
|
42
|
+
state_details = response.fetch('state_details')
|
43
|
+
unless state_details.fetch("is_final_state")
|
44
|
+
logger.info "Run #{run_id} is #{response['state']} and is #{response['current_progress']['percent']}% complete"
|
45
|
+
running = false if response["result"] == 'failed' && options.failfast?
|
46
|
+
else
|
47
|
+
logger.info "Run #{run_id} is now #{response["state"]} and has #{response["result"]}"
|
48
|
+
running = false
|
52
49
|
end
|
53
50
|
end
|
51
|
+
end
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
if url = response["frontend_url"]
|
54
|
+
logger.info "The detailed results are available at #{url}"
|
55
|
+
end
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
end
|
57
|
+
if response["result"] != "passed"
|
58
|
+
exit 1
|
62
59
|
end
|
60
|
+
end
|
63
61
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
62
|
+
def make_create_run_options
|
63
|
+
post_opts = {}
|
64
|
+
if options.git_trigger?
|
65
|
+
logger.debug "Checking last git commit message:"
|
66
|
+
commit_message = GitTrigger.last_commit_message
|
67
|
+
logger.debug commit_message
|
70
68
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
69
|
+
# Show some messages to users about tests/tags being overriden
|
70
|
+
unless options.tags.empty?
|
71
|
+
logger.warn "Specified tags are ignored when using --git-trigger"
|
72
|
+
else
|
73
|
+
logger.warn "Specified tests are ignored when using --git-trigger"
|
74
|
+
end
|
77
75
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
else
|
85
|
-
post_opts[:tags] = [tags.join(',')]
|
86
|
-
end
|
76
|
+
if GitTrigger.git_trigger_should_run?(commit_message)
|
77
|
+
tags = GitTrigger.extract_hashtags(commit_message)
|
78
|
+
if tags.empty?
|
79
|
+
logger.error "Triggered via git, but no hashtags detected. Please use commit message format:"
|
80
|
+
logger.error "\t'some message. @rainforest #tag1 #tag2"
|
81
|
+
exit 2
|
87
82
|
else
|
88
|
-
|
89
|
-
exit 0
|
83
|
+
post_opts[:tags] = [tags.join(',')]
|
90
84
|
end
|
91
85
|
else
|
92
|
-
|
93
|
-
|
94
|
-
post_opts[:tags] = options.tags
|
95
|
-
elsif !options.folder.nil?
|
96
|
-
post_opts[:smart_folder_id] = @options.folder.to_i
|
97
|
-
else
|
98
|
-
post_opts[:tests] = options.tests
|
99
|
-
end
|
86
|
+
logger.info "Not triggering as @rainforest was not mentioned in last commit message."
|
87
|
+
exit 0
|
100
88
|
end
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
post_opts[:
|
109
|
-
elsif options.environment_id
|
110
|
-
post_opts[:environment_id] = options.environment_id
|
89
|
+
else
|
90
|
+
# Not using git_trigger, so look for the
|
91
|
+
if !options.tags.empty?
|
92
|
+
post_opts[:tags] = options.tags
|
93
|
+
elsif !options.folder.nil?
|
94
|
+
post_opts[:smart_folder_id] = @options.folder.to_i
|
95
|
+
else
|
96
|
+
post_opts[:tests] = options.tests
|
111
97
|
end
|
112
|
-
|
113
|
-
post_opts
|
114
98
|
end
|
115
99
|
|
116
|
-
|
117
|
-
|
118
|
-
|
100
|
+
post_opts[:conflict] = options.conflict if options.conflict
|
101
|
+
post_opts[:browsers] = options.browsers if options.browsers
|
102
|
+
post_opts[:site_id] = options.site_id if options.site_id
|
103
|
+
post_opts[:description] = options.description if options.description
|
119
104
|
|
120
|
-
|
121
|
-
|
105
|
+
if options.custom_url
|
106
|
+
post_opts[:environment_id] = get_environment_id(options.custom_url)
|
107
|
+
elsif options.environment_id
|
108
|
+
post_opts[:environment_id] = options.environment_id
|
122
109
|
end
|
123
110
|
|
124
|
-
|
125
|
-
|
126
|
-
client.delete("generators/#{generator['id']}") if generator
|
127
|
-
end
|
111
|
+
post_opts
|
112
|
+
end
|
128
113
|
|
129
|
-
|
130
|
-
|
114
|
+
def logger
|
115
|
+
RainforestCli.logger
|
116
|
+
end
|
131
117
|
|
132
|
-
|
133
|
-
|
134
|
-
|
118
|
+
def list_generators
|
119
|
+
client.get("/generators")
|
120
|
+
end
|
135
121
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
end
|
122
|
+
def delete_generator(name)
|
123
|
+
generator = list_generators.find {|g| g['generator_type'] == 'tabular' && g['name'] == name }
|
124
|
+
client.delete("generators/#{generator['id']}") if generator
|
125
|
+
end
|
141
126
|
|
142
|
-
|
143
|
-
|
127
|
+
def url_valid?(url)
|
128
|
+
return false unless URI::regexp === url
|
144
129
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
logger.fatal "Error creating the ad-hoc URL: #{environment['error']}"
|
149
|
-
exit 1
|
150
|
-
end
|
130
|
+
uri = URI.parse(url)
|
131
|
+
%w(http https).include?(uri.scheme)
|
132
|
+
end
|
151
133
|
|
152
|
-
|
134
|
+
def get_environment_id url
|
135
|
+
unless url_valid?(url)
|
136
|
+
logger.fatal "The custom URL is invalid"
|
137
|
+
exit 2
|
153
138
|
end
|
139
|
+
|
140
|
+
env_post_body = { name: 'temporary-env-for-custom-url-via-CLI', url: url }
|
141
|
+
environment = client.post("/environments", env_post_body)
|
142
|
+
|
143
|
+
if environment['error']
|
144
|
+
# I am talking about a URL here because the environments are pretty
|
145
|
+
# much hidden from clients so far.
|
146
|
+
logger.fatal "Error creating the ad-hoc URL: #{environment['error']}"
|
147
|
+
exit 1
|
148
|
+
end
|
149
|
+
|
150
|
+
return environment['id']
|
154
151
|
end
|
155
152
|
end
|
156
153
|
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'rainforest'
|
3
|
+
require 'parallel'
|
4
|
+
require 'ruby-progressbar'
|
5
|
+
|
6
|
+
class RainforestCli::TestImporter
|
7
|
+
attr_reader :options, :client
|
8
|
+
SPEC_FOLDER = 'spec/rainforest'.freeze
|
9
|
+
EXT = ".rfml".freeze
|
10
|
+
THREADS = 32.freeze
|
11
|
+
|
12
|
+
SAMPLE_FILE = <<EOF
|
13
|
+
#! %s (this is the ID, don't edit it)
|
14
|
+
# title: New test
|
15
|
+
#
|
16
|
+
# 1. steps:
|
17
|
+
# a) pairs of lines are steps (first line = action, second = response)
|
18
|
+
# b) second line must have a ?
|
19
|
+
# c) second line must not be blank
|
20
|
+
# 2. comments:
|
21
|
+
# a) lines starting # are comments
|
22
|
+
#
|
23
|
+
|
24
|
+
EOF
|
25
|
+
|
26
|
+
def initialize(options)
|
27
|
+
@options = options
|
28
|
+
unless File.exists?(SPEC_FOLDER)
|
29
|
+
logger.fatal "Rainforest folder not found (#{SPEC_FOLDER})"
|
30
|
+
exit 2
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def logger
|
35
|
+
RainforestCli.logger
|
36
|
+
end
|
37
|
+
|
38
|
+
def export
|
39
|
+
::Rainforest.api_key = @options.token
|
40
|
+
|
41
|
+
tests = Rainforest::Test.all(page_size: 1000)
|
42
|
+
p = ProgressBar.create(title: 'Rows', total: tests.count, format: '%a %B %p%% %t')
|
43
|
+
Parallel.each(tests, in_threads: THREADS, finish: lambda { |item, i, result| p.increment }) do |test|
|
44
|
+
|
45
|
+
# File name
|
46
|
+
file_name = sprintf('%010d', test.id) + "_" + test.title.strip.gsub(/[^a-z0-9 ]+/i, '').gsub(/ +/, '_').downcase
|
47
|
+
file_name = create_new(file_name)
|
48
|
+
File.truncate(file_name, 0)
|
49
|
+
|
50
|
+
# Get the full test from the API
|
51
|
+
test = Rainforest::Test.retrieve(test.id)
|
52
|
+
|
53
|
+
File.open(file_name, 'a') do |file|
|
54
|
+
file.puts _get_header(test)
|
55
|
+
|
56
|
+
index = 0
|
57
|
+
test.elements.each do |element|
|
58
|
+
index = _process_element(file, element, index)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def _process_element file, element, index
|
65
|
+
case element[:type]
|
66
|
+
when 'test'
|
67
|
+
element[:element][:elements].each do |sub_element|
|
68
|
+
index = _process_element(file, sub_element, index)
|
69
|
+
end
|
70
|
+
when 'step'
|
71
|
+
file.puts "" unless index == 0
|
72
|
+
file.puts "# step #{index + 1}" if @options.debug
|
73
|
+
file.puts element[:element][:action]
|
74
|
+
file.puts element[:element][:response]
|
75
|
+
else
|
76
|
+
raise "Unknown element type: #{element[:type]}"
|
77
|
+
end
|
78
|
+
|
79
|
+
index += 1
|
80
|
+
index
|
81
|
+
end
|
82
|
+
|
83
|
+
# add comments if not already present
|
84
|
+
def _get_header test
|
85
|
+
out = []
|
86
|
+
|
87
|
+
has_id = false
|
88
|
+
test.description.to_s.strip.lines.map(&:chomp).each_with_index do |line, line_no|
|
89
|
+
line = line.gsub(/\#+$/, '').strip
|
90
|
+
|
91
|
+
# make sure the test has an ID
|
92
|
+
has_id = true if line[0] == "!"
|
93
|
+
|
94
|
+
out << "#" + line
|
95
|
+
end
|
96
|
+
|
97
|
+
unless has_id
|
98
|
+
browsers = test.browsers.map {|b| b[:name] if b[:state] == "enabled" }.compact
|
99
|
+
out = ["#! #{SecureRandom.uuid}", "# title: #{test.title}", "# start_uri: #{test.start_uri}", "# tags: #{test.tags.join(", ")}", "# browsers: #{browsers.join(", ")}", "#", " "] + out
|
100
|
+
end
|
101
|
+
|
102
|
+
out.compact.join("\n")
|
103
|
+
end
|
104
|
+
|
105
|
+
def _get_id test
|
106
|
+
id = nil
|
107
|
+
test.description.to_s.strip.lines.map(&:chomp).each_with_index do |line, line_no|
|
108
|
+
line = line.gsub(/\#+$/, '').strip
|
109
|
+
if line[0] == "!"
|
110
|
+
id = line[1..-1].split(' ').first
|
111
|
+
break
|
112
|
+
end
|
113
|
+
end
|
114
|
+
id
|
115
|
+
end
|
116
|
+
|
117
|
+
def upload
|
118
|
+
::Rainforest.api_key = @options.token
|
119
|
+
|
120
|
+
ids = {}
|
121
|
+
logger.info "Syncing tests"
|
122
|
+
Rainforest::Test.all(page_size: 1000).each do |test|
|
123
|
+
id = _get_id(test)
|
124
|
+
|
125
|
+
next if id.nil?
|
126
|
+
|
127
|
+
# note, this test id is numeric
|
128
|
+
ids[id] = test.id
|
129
|
+
end
|
130
|
+
|
131
|
+
logger.debug ids.inspect if @options.debug
|
132
|
+
|
133
|
+
tests = validate.values
|
134
|
+
|
135
|
+
logger.info "Uploading tests..."
|
136
|
+
p = ProgressBar.create(title: 'Rows', total: tests.count, format: '%a %B %p%% %t')
|
137
|
+
|
138
|
+
# Insert the data
|
139
|
+
Parallel.each(tests, in_threads: THREADS, finish: lambda { |item, i, result| p.increment }) do |test|
|
140
|
+
next unless test.steps.count > 0
|
141
|
+
|
142
|
+
if @options.debug
|
143
|
+
logger.debug "Starting: #{test.id}"
|
144
|
+
logger.debug "\t#{test.start_uri || "/"}"
|
145
|
+
end
|
146
|
+
|
147
|
+
test_obj = {
|
148
|
+
start_uri: test.start_uri || "/",
|
149
|
+
title: test.title,
|
150
|
+
description: test.description,
|
151
|
+
tags: (["ro"] + test.tags).uniq,
|
152
|
+
elements: test.steps.map do |step|
|
153
|
+
{type: 'step', redirection: true, element: {
|
154
|
+
action: step.action,
|
155
|
+
response: step.response
|
156
|
+
}}
|
157
|
+
end
|
158
|
+
}
|
159
|
+
|
160
|
+
unless test.browsers.empty?
|
161
|
+
test_obj[:browsers] = test.browsers.map {|b|
|
162
|
+
{'state': 'enabled', 'name': b}
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
# Create the test
|
167
|
+
begin
|
168
|
+
if ids[test.id]
|
169
|
+
t = Rainforest::Test.update(ids[test.id], test_obj)
|
170
|
+
|
171
|
+
logger.info "\tUpdated #{test.id} -- ##{t.id}" if @options.debug
|
172
|
+
else
|
173
|
+
t = Rainforest::Test.create(test_obj)
|
174
|
+
|
175
|
+
logger.info "\tCreated #{test.id} -- ##{t.id}" if @options.debug
|
176
|
+
end
|
177
|
+
rescue => e
|
178
|
+
logger.fatal "Error: #{test.id}: #{e}"
|
179
|
+
exit 2
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def validate
|
185
|
+
tests = {}
|
186
|
+
has_errors = []
|
187
|
+
|
188
|
+
Dir.glob("#{SPEC_FOLDER}/**/*#{EXT}").each do |file_name|
|
189
|
+
out = RainforestCli::TestParser::Parser.new(File.read(file_name)).process
|
190
|
+
|
191
|
+
tests[file_name] = out
|
192
|
+
has_errors << file_name if out.errors != {}
|
193
|
+
end
|
194
|
+
|
195
|
+
if !has_errors.empty?
|
196
|
+
logger.error "Parsing errors:"
|
197
|
+
logger.error ""
|
198
|
+
has_errors.each do |file_name|
|
199
|
+
logger.error " " + file_name
|
200
|
+
tests[file_name].errors.each do |line, error|
|
201
|
+
logger.error "\t#{error.to_s}"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
exit 2
|
206
|
+
end
|
207
|
+
|
208
|
+
if @options.debug
|
209
|
+
tests.each do |file_name,test|
|
210
|
+
logger.debug test.inspect
|
211
|
+
logger.debug "#{file_name}"
|
212
|
+
logger.debug test.description
|
213
|
+
test.steps.each do |step|
|
214
|
+
logger.debug "\t#{step}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
else
|
218
|
+
logger.info "[VALID]"
|
219
|
+
end
|
220
|
+
|
221
|
+
return tests
|
222
|
+
end
|
223
|
+
|
224
|
+
def create_new file_name = nil
|
225
|
+
name = @options.file_name if @options.file_name
|
226
|
+
name = file_name if !file_name.nil?
|
227
|
+
|
228
|
+
uuid = SecureRandom.uuid
|
229
|
+
name = "#{uuid}#{EXT}" unless name
|
230
|
+
name += EXT unless name[-EXT.length..-1] == EXT
|
231
|
+
name = File.join([SPEC_FOLDER, name])
|
232
|
+
|
233
|
+
File.open(name, "w") { |file| file.write(sprintf(SAMPLE_FILE, uuid)) }
|
234
|
+
|
235
|
+
logger.info "Created #{name}" if file_name.nil?
|
236
|
+
name
|
237
|
+
end
|
238
|
+
end
|