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.
@@ -1,156 +1,153 @@
1
- module Rainforest
2
- module Cli
3
- class Runner
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
- def run
12
- if options.import_file_name && options.import_name
13
- delete_generator(options.import_name)
14
- CSVImporter.new(options.import_name, options.import_file_name, options.token).import
15
- end
5
+ def initialize(options)
6
+ @options = options
7
+ @client = HttpClient.new token: options.token
8
+ end
16
9
 
17
- post_opts = make_create_run_options
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
- logger.debug "POST options: #{post_opts.inspect}"
20
- logger.info "Issuing run"
16
+ post_opts = make_create_run_options
21
17
 
22
- response = client.post('/runs', post_opts)
18
+ logger.debug "POST options: #{post_opts.inspect}"
19
+ logger.info "Issuing run"
23
20
 
24
- if response['error']
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
- if options.foreground?
31
- run_id = response.fetch("id")
32
- wait_for_run_completion(run_id)
33
- else
34
- true
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
- def wait_for_run_completion(run_id)
39
- running = true
40
- while running
41
- Kernel.sleep 5
42
- response = client.get("/runs/#{run_id}")
43
- if response
44
- state_details = response.fetch('state_details')
45
- unless state_details.fetch("is_final_state")
46
- logger.info "Run #{run_id} is #{response['state']} and is #{response['current_progress']['percent']}% complete"
47
- running = false if response["result"] == 'failed' && options.failfast?
48
- else
49
- logger.info "Run #{run_id} is now #{response["state"]} and has #{response["result"]}"
50
- running = false
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
- if url = response["frontend_url"]
56
- logger.info "The detailed results are available at #{url}"
57
- end
53
+ if url = response["frontend_url"]
54
+ logger.info "The detailed results are available at #{url}"
55
+ end
58
56
 
59
- if response["result"] != "passed"
60
- exit 1
61
- end
57
+ if response["result"] != "passed"
58
+ exit 1
62
59
  end
60
+ end
63
61
 
64
- def make_create_run_options
65
- post_opts = {}
66
- if options.git_trigger?
67
- logger.debug "Checking last git commit message:"
68
- commit_message = GitTrigger.last_commit_message
69
- logger.debug commit_message
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
- # Show some messages to users about tests/tags being overriden
72
- unless options.tags.empty?
73
- logger.warn "Specified tags are ignored when using --git-trigger"
74
- else
75
- logger.warn "Specified tests are ignored when using --git-trigger"
76
- end
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
- if GitTrigger.git_trigger_should_run?(commit_message)
79
- tags = GitTrigger.extract_hashtags(commit_message)
80
- if tags.empty?
81
- logger.error "Triggered via git, but no hashtags detected. Please use commit message format:"
82
- logger.error "\t'some message. @rainforest #tag1 #tag2"
83
- exit 2
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
- logger.info "Not triggering as @rainforest was not mentioned in last commit message."
89
- exit 0
83
+ post_opts[:tags] = [tags.join(',')]
90
84
  end
91
85
  else
92
- # Not using git_trigger, so look for the
93
- if !options.tags.empty?
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
- post_opts[:conflict] = options.conflict if options.conflict
103
- post_opts[:browsers] = options.browsers if options.browsers
104
- post_opts[:site_id] = options.site_id if options.site_id
105
- post_opts[:description] = options.description if options.description
106
-
107
- if options.custom_url
108
- post_opts[:environment_id] = get_environment_id(options.custom_url)
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
- def logger
117
- Rainforest::Cli.logger
118
- end
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
- def list_generators
121
- client.get("/generators")
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
- def delete_generator(name)
125
- generator = list_generators.find {|g| g['generator_type'] == 'tabular' && g['name'] == name }
126
- client.delete("generators/#{generator['id']}") if generator
127
- end
111
+ post_opts
112
+ end
128
113
 
129
- def url_valid?(url)
130
- return false unless URI::regexp === url
114
+ def logger
115
+ RainforestCli.logger
116
+ end
131
117
 
132
- uri = URI.parse(url)
133
- %w(http https).include?(uri.scheme)
134
- end
118
+ def list_generators
119
+ client.get("/generators")
120
+ end
135
121
 
136
- def get_environment_id url
137
- unless url_valid?(url)
138
- logger.fatal "The custom URL is invalid"
139
- exit 2
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
- env_post_body = { name: 'temporary-env-for-custom-url-via-CLI', url: url }
143
- environment = client.post("/environments", env_post_body)
127
+ def url_valid?(url)
128
+ return false unless URI::regexp === url
144
129
 
145
- if environment['error']
146
- # I am talking about a URL here because the environments are pretty
147
- # much hidden from clients so far.
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
- return environment['id']
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