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