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