guard-jasmine 2.0.0 → 2.0.1
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/lib/generators/guard_jasmine/install_generator.rb +2 -3
- data/lib/generators/guard_jasmine/templates/Guardfile +4 -4
- data/lib/guard/jasmine.rb +14 -21
- data/lib/guard/jasmine/cli.rb +5 -10
- data/lib/guard/jasmine/coverage.rb +7 -9
- data/lib/guard/jasmine/formatter.rb +8 -11
- data/lib/guard/jasmine/inspector.rb +1 -4
- data/lib/guard/jasmine/phantomjs/guard-jasmine.js +12 -4
- data/lib/guard/jasmine/phantomjs/src/guard-jasmine.coffee +14 -4
- data/lib/guard/jasmine/runner.rb +508 -523
- data/lib/guard/jasmine/server.rb +42 -43
- data/lib/guard/jasmine/task.rb +2 -5
- data/lib/guard/jasmine/util.rb +1 -5
- data/lib/guard/jasmine/version.rb +1 -1
- metadata +16 -58
data/lib/guard/jasmine/runner.rb
CHANGED
@@ -6,7 +6,6 @@ require 'guard/jasmine/util'
|
|
6
6
|
|
7
7
|
module Guard
|
8
8
|
class Jasmine
|
9
|
-
|
10
9
|
# The Jasmine runner handles the execution of the spec through the PhantomJS binary,
|
11
10
|
# evaluates the JSON response from the PhantomJS Script `guard_jasmine.coffee`,
|
12
11
|
# writes the result to the console and triggers optional system notifications.
|
@@ -44,7 +43,7 @@ module Guard
|
|
44
43
|
# Only specs with failures will be returned. Therefore an empty? return hash indicates success.
|
45
44
|
def run(paths, per_run_options = {})
|
46
45
|
previous_options = @options
|
47
|
-
@options.merge!(
|
46
|
+
@options.merge!(per_run_options)
|
48
47
|
|
49
48
|
return {} if paths.empty?
|
50
49
|
|
@@ -58,616 +57,602 @@ module Guard
|
|
58
57
|
# return the errors
|
59
58
|
return run_results.each_with_object({}) do | spec_run, hash |
|
60
59
|
file, r = spec_run
|
61
|
-
errors = collect_spec_errors(r['suites']||[])
|
62
|
-
errors.push(
|
60
|
+
errors = collect_spec_errors(r['suites'] || [])
|
61
|
+
errors.push(r['error']) if r.key? 'error'
|
63
62
|
hash[file] = errors unless errors.empty?
|
64
63
|
end
|
65
64
|
ensure
|
66
|
-
@options=previous_options
|
65
|
+
@options = previous_options
|
67
66
|
end
|
68
67
|
|
69
|
-
|
68
|
+
private
|
70
69
|
|
71
70
|
# Shows a notification in the console that the runner starts.
|
72
71
|
#
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
72
|
+
# @param [Array<String>] paths the spec files or directories
|
73
|
+
#
|
74
|
+
def notify_start_message(paths)
|
75
|
+
message = if paths == [options[:spec_dir]]
|
76
|
+
'Run all Jasmine suites'
|
77
|
+
else
|
78
|
+
"Run Jasmine suite#{ paths.size == 1 ? '' : 's' } #{ paths.join(' ') }"
|
79
|
+
end
|
80
|
+
|
81
|
+
Formatter.info(message, reset: true)
|
82
|
+
end
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
+
# Run the Jasmine spec by executing the PhantomJS script.
|
85
|
+
#
|
86
|
+
# @param [String] file the path of the spec
|
87
|
+
#
|
88
|
+
def run_jasmine_spec(file)
|
89
|
+
suite = jasmine_suite(file)
|
90
|
+
|
91
|
+
arguments = [
|
92
|
+
options[:timeout] * 1000,
|
93
|
+
options[:specdoc],
|
94
|
+
options[:focus],
|
95
|
+
options[:console],
|
96
|
+
options[:errors],
|
97
|
+
options[:junit],
|
98
|
+
options[:junit_consolidate],
|
99
|
+
"'#{ options[:junit_save_path] }'"
|
100
|
+
]
|
101
|
+
cmd = "#{ phantomjs_command } \"#{ suite }\" #{ arguments.collect(&:to_s).join(' ')}"
|
102
|
+
puts cmd if options[:debug]
|
103
|
+
IO.popen(cmd, 'r:UTF-8')
|
104
|
+
end
|
84
105
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
options[:timeout] * 1000,
|
94
|
-
options[:specdoc],
|
95
|
-
options[:focus],
|
96
|
-
options[:console],
|
97
|
-
options[:errors],
|
98
|
-
options[:junit],
|
99
|
-
options[:junit_consolidate],
|
100
|
-
"'#{ options[:junit_save_path] }'"
|
101
|
-
]
|
102
|
-
cmd = "#{ phantomjs_command } \"#{ suite }\" #{ arguments.collect { |i| i.to_s }.join(' ')}"
|
103
|
-
puts cmd if options[:debug]
|
104
|
-
IO.popen(cmd, 'r:UTF-8')
|
105
|
-
end
|
106
|
+
# Get the PhantomJS binary and script to execute.
|
107
|
+
#
|
108
|
+
# @return [String] the command
|
109
|
+
#
|
110
|
+
def phantomjs_command
|
111
|
+
options[:phantomjs_bin] + ' ' + phantomjs_script
|
112
|
+
# options[:phantomjs_bin] + ' --remote-debugger-port=9000 ' + phantomjs_script
|
113
|
+
end
|
106
114
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
+
# Get the Jasmine test runner URL with the appended suite name
|
116
|
+
# that acts as the spec filter.
|
117
|
+
#
|
118
|
+
# @param [String] file the spec file
|
119
|
+
# @return [String] the Jasmine url
|
120
|
+
#
|
121
|
+
def jasmine_suite(file)
|
122
|
+
options[:jasmine_url] + query_string_for_suite(file)
|
123
|
+
end
|
115
124
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
end
|
125
|
+
# Get the PhantomJS script that executes the spec and extracts
|
126
|
+
# the result from the headless DOM.
|
127
|
+
#
|
128
|
+
# @return [String] the path to the PhantomJS script
|
129
|
+
#
|
130
|
+
def phantomjs_script
|
131
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'phantomjs', 'guard-jasmine.js'))
|
132
|
+
end
|
125
133
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
+
# The suite name must be extracted from the spec that
|
135
|
+
# will be run.
|
136
|
+
#
|
137
|
+
# @param [String] file the spec file
|
138
|
+
# @return [String] the suite name
|
139
|
+
#
|
140
|
+
def query_string_for_suite(file)
|
141
|
+
params = {}
|
142
|
+
params.merge!(options[:query_params]) if options[:query_params]
|
134
143
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
# @param [String] file the spec file
|
139
|
-
# @return [String] the suite name
|
140
|
-
#
|
141
|
-
def query_string_for_suite(file)
|
142
|
-
params = {}
|
143
|
-
if options[:query_params]
|
144
|
-
params.merge!(options[:query_params])
|
145
|
-
end
|
146
|
-
if file != options[:spec_dir]
|
147
|
-
params[:spec] = suite_from_line_number(file) || suite_from_first_describe(file)
|
148
|
-
end
|
149
|
-
params.empty? ? "" : "?"+URI.encode_www_form(params).gsub('+','%20')
|
150
|
-
end
|
144
|
+
params[:spec] = suite_from_line_number(file) || suite_from_first_describe(file) if file != options[:spec_dir]
|
145
|
+
params.empty? ? '' : '?' + URI.encode_www_form(params).gsub('+', '%20')
|
146
|
+
end
|
151
147
|
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
if line_number
|
164
|
-
lines = it_and_describe_lines(file_name, 0, line_number)
|
165
|
-
last = lines.pop
|
166
|
-
|
167
|
-
last_indentation = last[/^\s*/].length
|
168
|
-
# keep only lines with lower indentation
|
169
|
-
lines.delete_if { |x| x[/^\s*/].length >= last_indentation }
|
170
|
-
# remove all 'it'
|
171
|
-
lines.delete_if { |x| x =~ /^\s*it/ }
|
172
|
-
|
173
|
-
lines << last
|
174
|
-
lines.map { |x| spec_title(x) }.join(' ')
|
175
|
-
end
|
176
|
-
end
|
148
|
+
# When providing a line number by either the option or by
|
149
|
+
# a number directly after the file name the suite is extracted
|
150
|
+
# fromt the corresponding line number in the file.
|
151
|
+
#
|
152
|
+
# @param [String] file the spec file
|
153
|
+
# @return [String] the suite name
|
154
|
+
#
|
155
|
+
def suite_from_line_number(file)
|
156
|
+
file_name, line_number = file_and_line_number_parts(file)
|
157
|
+
line_number ||= options[:line_number]
|
177
158
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
# found.
|
182
|
-
#
|
183
|
-
# @param [String] file the spec file
|
184
|
-
# @return [String] the suite name
|
185
|
-
#
|
186
|
-
def suite_from_first_describe(file)
|
187
|
-
File.foreach(file) do |line|
|
188
|
-
if line =~ /describe\s*[("']+(.*?)["')]+/ #'
|
189
|
-
return $1
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
159
|
+
if line_number
|
160
|
+
lines = it_and_describe_lines(file_name, 0, line_number)
|
161
|
+
last = lines.pop
|
193
162
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
# returned array is `nil`.
|
200
|
-
#
|
201
|
-
# @param [String] file the spec file
|
202
|
-
# @return [Array] `[file_name, line_number]`
|
203
|
-
#
|
204
|
-
def file_and_line_number_parts(file)
|
205
|
-
match = file.match(/^(.+?)(?::(\d+))?$/)
|
206
|
-
[match[1], match[2].nil? ? nil : match[2].to_i]
|
207
|
-
end
|
163
|
+
last_indentation = last[/^\s*/].length
|
164
|
+
# keep only lines with lower indentation
|
165
|
+
lines.delete_if { |x| x[/^\s*/].length >= last_indentation }
|
166
|
+
# remove all 'it'
|
167
|
+
lines.delete_if { |x| x =~ /^\s*it/ }
|
208
168
|
|
209
|
-
|
210
|
-
|
211
|
-
#
|
212
|
-
# @param [String] file the spec file
|
213
|
-
# @param [Numeric] from the first line in the range
|
214
|
-
# @param [Numeric] to the last line in the range
|
215
|
-
# @Return [Array] the line contents
|
216
|
-
#
|
217
|
-
def it_and_describe_lines(file, from, to)
|
218
|
-
File.readlines(file)[from, to].
|
219
|
-
select { |x| x =~ /^\s*(it|describe)/ }
|
169
|
+
lines << last
|
170
|
+
lines.map { |x| spec_title(x) }.join(' ')
|
220
171
|
end
|
172
|
+
end
|
221
173
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
174
|
+
# The suite name must be extracted from the spec that
|
175
|
+
# will be run. This is done by parsing from the head of
|
176
|
+
# the spec file until the first `describe` function is
|
177
|
+
# found.
|
178
|
+
#
|
179
|
+
# @param [String] file the spec file
|
180
|
+
# @return [String] the suite name
|
181
|
+
#
|
182
|
+
def suite_from_first_describe(file)
|
183
|
+
File.foreach(file) do |line|
|
184
|
+
return Regexp.last_match[1] if line =~ /describe\s*[("']+(.*?)["')]+/ # '
|
229
185
|
end
|
186
|
+
end
|
230
187
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
pp result if options[:debug]
|
246
|
-
if result['error']
|
247
|
-
if options[:is_cli]
|
248
|
-
raise 'An error occurred in the Jasmine runner'
|
249
|
-
else
|
250
|
-
notify_runtime_error(result)
|
251
|
-
end
|
252
|
-
elsif result
|
253
|
-
result['file'] = file
|
254
|
-
notify_spec_result(result)
|
255
|
-
end
|
188
|
+
# Splits the file name into the physical file name
|
189
|
+
# and the line number if present. E.g.:
|
190
|
+
# 'some_spec.js.coffee:10' -> ['some_spec.js.coffee', 10].
|
191
|
+
#
|
192
|
+
# If the line number is missing the second part of the
|
193
|
+
# returned array is `nil`.
|
194
|
+
#
|
195
|
+
# @param [String] file the spec file
|
196
|
+
# @return [Array] `[file_name, line_number]`
|
197
|
+
#
|
198
|
+
def file_and_line_number_parts(file)
|
199
|
+
match = file.match(/^(.+?)(?::(\d+))?$/)
|
200
|
+
[match[1], match[2].nil? ? nil : match[2].to_i]
|
201
|
+
end
|
256
202
|
|
257
|
-
|
258
|
-
|
259
|
-
|
203
|
+
# Returns all lines of the file that are either a
|
204
|
+
# 'describe' or a 'it' declaration.
|
205
|
+
#
|
206
|
+
# @param [String] file the spec file
|
207
|
+
# @param [Numeric] from the first line in the range
|
208
|
+
# @param [Numeric] to the last line in the range
|
209
|
+
# @Return [Array] the line contents
|
210
|
+
#
|
211
|
+
def it_and_describe_lines(file, from, to)
|
212
|
+
File.readlines(file)[from, to]
|
213
|
+
.select { |x| x =~ /^\s*(it|describe)/ }
|
214
|
+
end
|
260
215
|
|
261
|
-
|
216
|
+
# Extracts the title of a 'description' or a 'it' declaration.
|
217
|
+
#
|
218
|
+
# @param [String] the line content
|
219
|
+
# @return [String] the extracted title
|
220
|
+
#
|
221
|
+
def spec_title(line)
|
222
|
+
line[/['"](.+?)["']/, 1]
|
223
|
+
end
|
262
224
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
225
|
+
# Evaluates the JSON response that the PhantomJS script
|
226
|
+
# writes to stdout. The results triggers further notification
|
227
|
+
# actions.
|
228
|
+
#
|
229
|
+
# @param [String] output the JSON output the spec run
|
230
|
+
# @param [String] file the file name of the spec
|
231
|
+
# @return [Hash] results of the suite's specs
|
232
|
+
#
|
233
|
+
def evaluate_response(output, file)
|
234
|
+
json = output.read
|
235
|
+
json = json.encode('UTF-8') if json.respond_to?(:encode)
|
236
|
+
begin
|
237
|
+
result = MultiJson.decode(json, max_nesting: false)
|
238
|
+
fail 'No response from Jasmine runner' if !result && options[:is_cli]
|
239
|
+
pp result if options[:debug]
|
240
|
+
if result['error']
|
241
|
+
if options[:is_cli]
|
242
|
+
fail 'An error occurred in the Jasmine runner'
|
270
243
|
else
|
271
|
-
|
272
|
-
raise "Cannot decode JSON from PhantomJS runner, message received was:\n#{json}"
|
273
|
-
else
|
274
|
-
Formatter.error("Cannot decode JSON from PhantomJS runner: #{ e.message }")
|
275
|
-
Formatter.error("JSON response: #{ e.data }")
|
276
|
-
Formatter.error("message received was:\n#{json}")
|
277
|
-
end
|
244
|
+
notify_runtime_error(result)
|
278
245
|
end
|
279
|
-
|
280
|
-
|
246
|
+
elsif result
|
247
|
+
result['file'] = file
|
248
|
+
notify_spec_result(result)
|
281
249
|
end
|
282
|
-
end
|
283
|
-
|
284
|
-
# Notification when a system error happens that
|
285
|
-
# prohibits the execution of the Jasmine spec.
|
286
|
-
#
|
287
|
-
# @param [Hash] result the suite result
|
288
|
-
#
|
289
|
-
def notify_runtime_error(result)
|
290
|
-
message = "An error occurred: #{ result['error'] }"
|
291
|
-
Formatter.error(message )
|
292
|
-
Formatter.error( result['trace'] ) if result['trace']
|
293
|
-
Formatter.notify(message, title: 'Jasmine error', image: :failed, priority: 2) if options[:notification]
|
294
|
-
end
|
295
|
-
|
296
|
-
# Notification about a spec run, success or failure,
|
297
|
-
# and some stats.
|
298
|
-
#
|
299
|
-
# @param [Hash] result the suite result
|
300
|
-
#
|
301
|
-
def notify_spec_result(result)
|
302
|
-
specs = result['stats']['specs'] - result['stats']['disabled']
|
303
|
-
failed = result['stats']['failed']
|
304
|
-
time = sprintf( '%0.2f', result['stats']['time'] )
|
305
|
-
specs_plural = specs == 1 ? '' : 's'
|
306
|
-
failed_plural = failed == 1 ? '' : 's'
|
307
|
-
Formatter.info("Finished in #{ time } seconds")
|
308
|
-
pending = result['stats']['pending'].to_i > 0 ? " #{result['stats']['pending']} pending," : ""
|
309
|
-
message = "#{ specs } spec#{ specs_plural },#{pending} #{ failed } failure#{ failed_plural }"
|
310
|
-
full_message = "#{ message }\nin #{ time } seconds"
|
311
|
-
passed = failed == 0
|
312
|
-
|
313
|
-
report_specdoc(result, passed) if specdoc_shown?(passed)
|
314
|
-
|
315
|
-
if passed
|
316
|
-
Formatter.success(message)
|
317
|
-
Formatter.notify(full_message, title: 'Jasmine suite passed') if options[:notification] && !options[:hide_success]
|
318
|
-
else
|
319
|
-
errors = collect_spec_errors(result['suites'])
|
320
|
-
error_message = errors[0..options[:max_error_notify]].join("\n")
|
321
250
|
|
322
|
-
|
323
|
-
|
324
|
-
Formatter.notify( "#{error_message}\n#{full_message}",
|
325
|
-
title: 'Jasmine suite failed', image: :failed, priority: 2)
|
326
|
-
end
|
251
|
+
if result && result['coverage'] && options[:coverage]
|
252
|
+
notify_coverage_result(result['coverage'], file)
|
327
253
|
end
|
328
254
|
|
329
|
-
|
330
|
-
|
331
|
-
# Notification about the coverage of a spec run, success or failed,
|
332
|
-
# and some stats.
|
333
|
-
#
|
334
|
-
# @param [Hash] coverage the coverage hash from the JSON
|
335
|
-
# @param [String] file the file name of the spec
|
336
|
-
#
|
337
|
-
def notify_coverage_result(coverage, file)
|
338
|
-
if coverage_bin
|
339
|
-
FileUtils.mkdir_p(coverage_root) unless File.exist?(coverage_root)
|
255
|
+
return result
|
340
256
|
|
341
|
-
|
342
|
-
|
343
|
-
if options[:
|
344
|
-
|
257
|
+
rescue MultiJson::DecodeError => e
|
258
|
+
if e.data == ''
|
259
|
+
if options[:is_cli]
|
260
|
+
raise 'No response from Jasmine runner'
|
345
261
|
else
|
346
|
-
|
347
|
-
end
|
348
|
-
|
349
|
-
check_coverage
|
350
|
-
|
351
|
-
if options[:coverage_html]
|
352
|
-
generate_html_report
|
262
|
+
Formatter.error('No response from the Jasmine runner!')
|
353
263
|
end
|
354
264
|
else
|
355
|
-
|
265
|
+
if options[:is_cli]
|
266
|
+
raise "Cannot decode JSON from PhantomJS runner, message received was:\n#{json}"
|
267
|
+
else
|
268
|
+
Formatter.error("Cannot decode JSON from PhantomJS runner: #{ e.message }")
|
269
|
+
Formatter.error("JSON response: #{ e.data }")
|
270
|
+
Formatter.error("message received was:\n#{json}")
|
271
|
+
end
|
356
272
|
end
|
273
|
+
ensure
|
274
|
+
output.close
|
357
275
|
end
|
276
|
+
end
|
358
277
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
278
|
+
# Notification when a system error happens that
|
279
|
+
# prohibits the execution of the Jasmine spec.
|
280
|
+
#
|
281
|
+
# @param [Hash] result the suite result
|
282
|
+
#
|
283
|
+
def notify_runtime_error(result)
|
284
|
+
message = "An error occurred: #{ result['error'] }"
|
285
|
+
Formatter.error(message)
|
286
|
+
Formatter.error(result['trace']) if result['trace']
|
287
|
+
Formatter.notify(message, title: 'Jasmine error', image: :failed, priority: 2) if options[:notification]
|
288
|
+
end
|
366
289
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
290
|
+
# Notification about a spec run, success or failure,
|
291
|
+
# and some stats.
|
292
|
+
#
|
293
|
+
# @param [Hash] result the suite result
|
294
|
+
#
|
295
|
+
def notify_spec_result(result)
|
296
|
+
specs = result['stats']['specs'] - result['stats']['disabled']
|
297
|
+
failed = result['stats']['failed']
|
298
|
+
time = sprintf('%0.2f', result['stats']['time'])
|
299
|
+
specs_plural = specs == 1 ? '' : 's'
|
300
|
+
failed_plural = failed == 1 ? '' : 's'
|
301
|
+
Formatter.info("Finished in #{ time } seconds")
|
302
|
+
pending = result['stats']['pending'].to_i > 0 ? " #{result['stats']['pending']} pending," : ''
|
303
|
+
message = "#{ specs } spec#{ specs_plural },#{pending} #{ failed } failure#{ failed_plural }"
|
304
|
+
full_message = "#{ message }\nin #{ time } seconds"
|
305
|
+
passed = failed == 0
|
306
|
+
|
307
|
+
report_specdoc(result, passed) if specdoc_shown?(passed)
|
308
|
+
|
309
|
+
if passed
|
310
|
+
Formatter.success(message)
|
311
|
+
Formatter.notify(full_message, title: 'Jasmine suite passed') if options[:notification] && !options[:hide_success]
|
312
|
+
else
|
313
|
+
errors = collect_spec_errors(result['suites'])
|
314
|
+
error_message = errors[0..options[:max_error_notify]].join("\n")
|
315
|
+
|
316
|
+
Formatter.error(message)
|
317
|
+
if options[:notification]
|
318
|
+
Formatter.notify("#{error_message}\n#{full_message}",
|
319
|
+
title: 'Jasmine suite failed', image: :failed, priority: 2)
|
372
320
|
end
|
321
|
+
end
|
322
|
+
end
|
373
323
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
324
|
+
# Notification about the coverage of a spec run, success or failed,
|
325
|
+
# and some stats.
|
326
|
+
#
|
327
|
+
# @param [Hash] coverage the coverage hash from the JSON
|
328
|
+
# @param [String] file the file name of the spec
|
329
|
+
#
|
330
|
+
def notify_coverage_result(coverage, file)
|
331
|
+
if coverage_bin
|
332
|
+
FileUtils.mkdir_p(coverage_root) unless File.exist?(coverage_root)
|
379
333
|
|
380
|
-
|
381
|
-
end
|
334
|
+
update_coverage(coverage, file)
|
382
335
|
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
if any_coverage_threshold?
|
388
|
-
coverage = `#{coverage_bin} check-coverage #{ istanbul_coverage_options } #{ coverage_file } 2>&1`
|
389
|
-
coverage = coverage.split("\n").grep(/ERROR/).join.sub('ERROR:', '')
|
390
|
-
failed = $? && $?.exitstatus != 0
|
391
|
-
|
392
|
-
if failed
|
393
|
-
Formatter.error coverage
|
394
|
-
Formatter.notify(coverage, title: 'Code coverage failed', image: :failed, priority: 2) if options[:notification]
|
395
|
-
else
|
396
|
-
Formatter.success 'Code coverage succeed'
|
397
|
-
Formatter.notify('All code is adequately covered with specs', title: 'Code coverage succeed') if options[:notification] && !options[:hide_success]
|
398
|
-
end
|
336
|
+
if options[:coverage_summary]
|
337
|
+
generate_summary_report
|
338
|
+
else
|
339
|
+
generate_text_report(file)
|
399
340
|
end
|
400
|
-
end
|
401
341
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
`#{coverage_bin} report --dir #{ report_directory } --root #{ coverage_root } html #{ coverage_file }`
|
408
|
-
Formatter.info "Updated HTML report available at: #{ report_directory }/index.html"
|
342
|
+
check_coverage
|
343
|
+
|
344
|
+
generate_html_report if options[:coverage_html]
|
345
|
+
else
|
346
|
+
Formatter.error('Skipping coverage report: unable to locate istanbul in your PATH')
|
409
347
|
end
|
348
|
+
end
|
410
349
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
350
|
+
# Uses the Istanbul text reported to output the result of the
|
351
|
+
# last coverage run.
|
352
|
+
#
|
353
|
+
# @param [String] file the file name of the spec
|
354
|
+
#
|
355
|
+
def generate_text_report(file)
|
356
|
+
Formatter.info 'Spec coverage details:'
|
416
357
|
|
417
|
-
|
358
|
+
if file == options[:spec_dir]
|
359
|
+
matcher = /[|+]$/
|
360
|
+
else
|
361
|
+
impl = file.sub('_spec', '').sub(options[:spec_dir], '')
|
362
|
+
matcher = /(-+|All files|% Lines|#{ Regexp.escape(File.basename(impl)) }|#{ File.dirname(impl).sub(/^\//, '') }\/[^\/])/
|
363
|
+
end
|
418
364
|
|
419
|
-
|
420
|
-
puts line.sub(/\n$/, '') if line =~ /\)$/
|
421
|
-
end
|
365
|
+
puts ''
|
422
366
|
|
423
|
-
|
367
|
+
`#{coverage_bin} report --root #{ coverage_root } text #{ coverage_file }`.each_line do |line|
|
368
|
+
puts line.sub(/\n$/, '') if line =~ matcher
|
424
369
|
end
|
425
370
|
|
426
|
-
|
427
|
-
|
428
|
-
# @param [Hash] result the suite result
|
429
|
-
# @param [Boolean] passed status
|
430
|
-
#
|
431
|
-
def report_specdoc(result, passed)
|
432
|
-
result['suites'].each do |suite|
|
433
|
-
report_specdoc_suite(suite, passed)
|
434
|
-
end
|
435
|
-
end
|
371
|
+
puts ''
|
372
|
+
end
|
436
373
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
next unless :always==options[:specdoc] || spec['status'] == 'failed' || ( !run_passed && !options[:focus] )
|
453
|
-
if spec['status'] == 'passed'
|
454
|
-
Formatter.success(indent(" ✔ #{ spec['description'] }", level))
|
455
|
-
elsif spec['status'] == 'failed'
|
456
|
-
Formatter.spec_failed(indent(" ✘ #{ spec['description'] }", level))
|
457
|
-
else
|
458
|
-
Formatter.spec_pending(indent(" ○ #{ spec['description'] }", level))
|
459
|
-
end
|
460
|
-
report_specdoc_errors(spec, level)
|
461
|
-
report_specdoc_logs(spec, level)
|
374
|
+
# Uses the Istanbul text reported to output the result of the
|
375
|
+
# last coverage run.
|
376
|
+
#
|
377
|
+
def check_coverage
|
378
|
+
if any_coverage_threshold?
|
379
|
+
coverage = `#{coverage_bin} check-coverage #{ istanbul_coverage_options } #{ coverage_file } 2>&1`
|
380
|
+
coverage = coverage.split("\n").grep(/ERROR/).join.sub('ERROR:', '')
|
381
|
+
failed = $CHILD_STATUS && $CHILD_STATUS.exitstatus != 0
|
382
|
+
|
383
|
+
if failed
|
384
|
+
Formatter.error coverage
|
385
|
+
Formatter.notify(coverage, title: 'Code coverage failed', image: :failed, priority: 2) if options[:notification]
|
386
|
+
else
|
387
|
+
Formatter.success 'Code coverage succeed'
|
388
|
+
Formatter.notify('All code is adequately covered with specs', title: 'Code coverage succeed') if options[:notification] && !options[:hide_success]
|
462
389
|
end
|
463
|
-
|
464
|
-
suite['suites'].each { |s| report_specdoc_suite(s, run_passed, level + 2) } if suite['suites']
|
465
|
-
end
|
466
|
-
|
467
|
-
# Is the specdoc shown for this suite?
|
468
|
-
#
|
469
|
-
# @param [Boolean] passed the spec status
|
470
|
-
#
|
471
|
-
def specdoc_shown?(passed)
|
472
|
-
options[:specdoc] == :always || (options[:specdoc] == :failure && !passed)
|
473
390
|
end
|
391
|
+
end
|
474
392
|
|
393
|
+
# Uses the Istanbul text reported to output the result of the
|
394
|
+
# last coverage run.
|
395
|
+
#
|
396
|
+
def generate_html_report
|
397
|
+
report_directory = coverage_report_directory
|
398
|
+
`#{coverage_bin} report --dir #{ report_directory } --root #{ coverage_root } html #{ coverage_file }`
|
399
|
+
Formatter.info "Updated HTML report available at: #{ report_directory }/index.html"
|
400
|
+
end
|
475
401
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
spec['logs'] && (( spec['status'] == 'passed' && options[:console] == :always) ||
|
482
|
-
(spec['status'] == 'failed' && options[:console] != :never) )
|
483
|
-
end
|
402
|
+
# Uses the Istanbul text-summary reporter to output the
|
403
|
+
# summary of all the coverage runs combined.
|
404
|
+
#
|
405
|
+
def generate_summary_report
|
406
|
+
Formatter.info 'Spec coverage summary:'
|
484
407
|
|
408
|
+
puts ''
|
485
409
|
|
486
|
-
#
|
487
|
-
|
488
|
-
# @param [Hash] spec the spec
|
489
|
-
def errors_for_spec?(spec)
|
490
|
-
spec['errors'] && ((spec['status']=='passed' && options[:errors] == :always) ||
|
491
|
-
(spec['status']=='failed' && options[:errors] != :never))
|
410
|
+
`#{coverage_bin} report --root #{ coverage_root } text-summary #{ coverage_file }`.each_line do |line|
|
411
|
+
puts line.sub(/\n$/, '') if line =~ /\)$/
|
492
412
|
end
|
493
413
|
|
494
|
-
|
495
|
-
|
496
|
-
# @param [Boolean] passed the spec status
|
497
|
-
# @param [Hash] spec the spec
|
498
|
-
#
|
499
|
-
def description_shown?(passed, spec)
|
500
|
-
specdoc_shown?(passed) || console_for_spec?(spec) || errors_for_spec?(spec)
|
501
|
-
end
|
414
|
+
puts ''
|
415
|
+
end
|
502
416
|
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
log_level = log_level == 'log' ? '' : "#{log_level.upcase}: "
|
512
|
-
Formatter.info(indent(" • #{log_level}#{ message }", level))
|
513
|
-
end
|
514
|
-
end
|
417
|
+
# Specdoc like formatting of the result.
|
418
|
+
#
|
419
|
+
# @param [Hash] result the suite result
|
420
|
+
# @param [Boolean] passed status
|
421
|
+
#
|
422
|
+
def report_specdoc(result, passed)
|
423
|
+
result['suites'].each do |suite|
|
424
|
+
report_specdoc_suite(suite, passed)
|
515
425
|
end
|
426
|
+
end
|
516
427
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
428
|
+
# Show the suite result.
|
429
|
+
#
|
430
|
+
# @param [Hash] suite the suite
|
431
|
+
# @param [Boolean] passed status
|
432
|
+
# @param [Number] level the indention level
|
433
|
+
#
|
434
|
+
def report_specdoc_suite(suite, run_passed, level = 0)
|
435
|
+
# Print the suite description when the specdoc is shown or there are logs to display
|
436
|
+
Formatter.suite_name((' ' * level) + suite['description'])
|
437
|
+
|
438
|
+
suite['specs'].each do |spec|
|
439
|
+
# Specs are shown if they failed, or if they passed and the "focus" option is falsey
|
440
|
+
# If specs are going to be shown, then pending are also shown
|
441
|
+
# If the focus option is set, then only failing tests are shown
|
442
|
+
next unless :always == options[:specdoc] || spec['status'] == 'failed' || (!run_passed && !options[:focus])
|
443
|
+
if spec['status'] == 'passed'
|
444
|
+
Formatter.success(indent(" ✔ #{ spec['description'] }", level))
|
445
|
+
elsif spec['status'] == 'failed'
|
446
|
+
Formatter.spec_failed(indent(" ✘ #{ spec['description'] }", level))
|
447
|
+
else
|
448
|
+
Formatter.spec_pending(indent(" ○ #{ spec['description'] }", level))
|
532
449
|
end
|
450
|
+
report_specdoc_errors(spec, level)
|
451
|
+
report_specdoc_logs(spec, level)
|
533
452
|
end
|
534
453
|
|
535
|
-
|
536
|
-
|
537
|
-
# @param [String] message the message
|
538
|
-
# @param [Number] level the indention level
|
539
|
-
#
|
540
|
-
def indent(message, level)
|
541
|
-
(' ' * level) + message
|
542
|
-
end
|
454
|
+
suite['suites'].each { |s| report_specdoc_suite(s, run_passed, level + 2) } if suite['suites']
|
455
|
+
end
|
543
456
|
|
457
|
+
# Is the specdoc shown for this suite?
|
458
|
+
#
|
459
|
+
# @param [Boolean] passed the spec status
|
460
|
+
#
|
461
|
+
def specdoc_shown?(passed)
|
462
|
+
options[:specdoc] == :always || (options[:specdoc] == :failure && !passed)
|
463
|
+
end
|
544
464
|
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
465
|
+
# Are console logs shown for this spec?
|
466
|
+
#
|
467
|
+
# @param [Hash] spec the spec
|
468
|
+
#
|
469
|
+
def console_for_spec?(spec)
|
470
|
+
spec['logs'] && ((spec['status'] == 'passed' && options[:console] == :always) ||
|
471
|
+
(spec['status'] == 'failed' && options[:console] != :never))
|
472
|
+
end
|
553
473
|
|
474
|
+
# Are errors shown for this spec?
|
475
|
+
#
|
476
|
+
# @param [Hash] spec the spec
|
477
|
+
def errors_for_spec?(spec)
|
478
|
+
spec['errors'] && ((spec['status'] == 'passed' && options[:errors] == :always) ||
|
479
|
+
(spec['status'] == 'failed' && options[:errors] != :never))
|
480
|
+
end
|
554
481
|
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
}.flatten
|
564
|
-
end
|
482
|
+
# Is the description shown for this spec?
|
483
|
+
#
|
484
|
+
# @param [Boolean] passed the spec status
|
485
|
+
# @param [Hash] spec the spec
|
486
|
+
#
|
487
|
+
def description_shown?(passed, spec)
|
488
|
+
specdoc_shown?(passed) || console_for_spec?(spec) || errors_for_spec?(spec)
|
489
|
+
end
|
565
490
|
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
491
|
+
# Shows the logs for a given spec.
|
492
|
+
#
|
493
|
+
# @param [Hash] spec the spec result
|
494
|
+
# @param [Number] level the indention level
|
495
|
+
#
|
496
|
+
def report_specdoc_logs(spec, level)
|
497
|
+
if console_for_spec?(spec)
|
498
|
+
spec['logs'].each do |log_level, message|
|
499
|
+
log_level = log_level == 'log' ? '' : "#{log_level.upcase}: "
|
500
|
+
Formatter.info(indent(" • #{log_level}#{ message }", level))
|
575
501
|
end
|
576
502
|
end
|
503
|
+
end
|
577
504
|
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
505
|
+
# Shows the errors for a given spec.
|
506
|
+
#
|
507
|
+
# @param [Hash] spec the spec result
|
508
|
+
# @param [Number] level the indention level
|
509
|
+
#
|
510
|
+
def report_specdoc_errors(spec, level)
|
511
|
+
return unless spec['errors'] && (options[:errors] == :always || (options[:errors] == :failure && spec['status'] == 'failed'))
|
512
|
+
|
513
|
+
spec['errors'].each do |error|
|
514
|
+
Formatter.spec_failed(indent(" ➤ #{ format_error(error, true) }", level))
|
515
|
+
next unless error['trace']
|
516
|
+
|
517
|
+
error['trace'].each do |trace|
|
518
|
+
Formatter.spec_failed(indent(" ➜ #{ trace['file'] } on line #{ trace['line'] }", level + 2))
|
591
519
|
end
|
592
520
|
end
|
521
|
+
end
|
593
522
|
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
File.write(coverage_file, MultiJson.encode(coverage, { max_nesting: false }))
|
603
|
-
else
|
604
|
-
if File.exist?(coverage_file)
|
605
|
-
impl = file.sub('_spec', '').sub(options[:spec_dir], '')
|
606
|
-
coverage = MultiJson.decode(File.read(coverage_file), { max_nesting: false })
|
523
|
+
# Indent a message.
|
524
|
+
#
|
525
|
+
# @param [String] message the message
|
526
|
+
# @param [Number] level the indention level
|
527
|
+
#
|
528
|
+
def indent(message, level)
|
529
|
+
(' ' * level) + message
|
530
|
+
end
|
607
531
|
|
608
|
-
|
609
|
-
|
610
|
-
|
532
|
+
# Tests if the given suite has a failing spec underneath.
|
533
|
+
#
|
534
|
+
# @param [Hash] suite the suite result
|
535
|
+
# @return [Boolean] the search result
|
536
|
+
#
|
537
|
+
def contains_failed_spec?(suite)
|
538
|
+
collect_specs([suite]).any? { |spec| spec['status'] == 'failed' }
|
539
|
+
end
|
611
540
|
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
541
|
+
# Get all failed specs from the suites and its nested suites.
|
542
|
+
#
|
543
|
+
# @param suites [Array<Hash>] the suites results
|
544
|
+
# @return [Array<Hash>] all failed
|
545
|
+
#
|
546
|
+
def collect_spec_errors(suites)
|
547
|
+
collect_specs(suites).map do |spec|
|
548
|
+
(spec['errors'] || []).map { |error| format_error(error, false) }
|
549
|
+
end.flatten
|
550
|
+
end
|
618
551
|
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
552
|
+
# Get all specs from the suites and its nested suites.
|
553
|
+
#
|
554
|
+
# @param suites [Array<Hash>] the suites results
|
555
|
+
# @return [Array<Hash>] all specs
|
556
|
+
#
|
557
|
+
def collect_specs(suites)
|
558
|
+
suites.each_with_object([]) do |suite, specs|
|
559
|
+
specs.concat(suite['specs'])
|
560
|
+
specs.concat(collect_specs(suite['suites'])) if suite['suites']
|
625
561
|
end
|
562
|
+
end
|
626
563
|
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
564
|
+
# Formats a message.
|
565
|
+
#
|
566
|
+
# @param [String] message the error message
|
567
|
+
# @param [Boolean] short show a short version of the message
|
568
|
+
# @return [String] the cleaned error message
|
569
|
+
#
|
570
|
+
def format_error(error, short)
|
571
|
+
message = error['message'].gsub(%r{ in http.*\(line \d+\)$}, '')
|
572
|
+
if !short && error['trace'] && error['trace'].length > 0
|
573
|
+
location = error['trace'][0]
|
574
|
+
"#{message} in #{location['file']}:#{location['line']}"
|
575
|
+
else
|
576
|
+
message
|
636
577
|
end
|
578
|
+
end
|
637
579
|
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
580
|
+
# Updates the coverage data with new data for the implementation file.
|
581
|
+
# It replaces the coverage data if the file is the spec dir.
|
582
|
+
#
|
583
|
+
# @param [Hash] coverage the last run coverage data
|
584
|
+
# @param [String] file the file name of the spec
|
585
|
+
#
|
586
|
+
def update_coverage(coverage, file)
|
587
|
+
if file == options[:spec_dir]
|
588
|
+
File.write(coverage_file, MultiJson.encode(coverage, max_nesting: false))
|
589
|
+
else
|
590
|
+
if File.exist?(coverage_file)
|
591
|
+
impl = file.sub('_spec', '').sub(options[:spec_dir], '')
|
592
|
+
coverage = MultiJson.decode(File.read(coverage_file), max_nesting: false)
|
593
|
+
|
594
|
+
coverage.each do |coverage_file, data|
|
595
|
+
coverage[coverage_file] = data if coverage_file == impl
|
596
|
+
end
|
645
597
|
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
#
|
651
|
-
def coverage_file
|
652
|
-
File.join(coverage_root, 'coverage.json')
|
598
|
+
File.write(coverage_file, MultiJson.encode(coverage, max_nesting: false))
|
599
|
+
else
|
600
|
+
File.write(coverage_file, MultiJson.encode({}))
|
601
|
+
end
|
653
602
|
end
|
603
|
+
end
|
654
604
|
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
605
|
+
# Do we should check the coverage?
|
606
|
+
#
|
607
|
+
# @return [Boolean] true if any coverage threshold is set
|
608
|
+
#
|
609
|
+
def any_coverage_threshold?
|
610
|
+
THRESHOLDS.any? { |threshold| options[threshold] != 0 }
|
611
|
+
end
|
662
612
|
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
613
|
+
# Converts the options to Istanbul recognized options
|
614
|
+
#
|
615
|
+
# @return [String] the command line options
|
616
|
+
#
|
617
|
+
def istanbul_coverage_options
|
618
|
+
THRESHOLDS.inject([]) do |coverage, name|
|
619
|
+
threshold = options[name]
|
620
|
+
coverage << (threshold != 0 ? "--#{ name.to_s.sub('_threshold', '') } #{ threshold }" : '')
|
621
|
+
end.reject(&:empty?).join(' ')
|
622
|
+
end
|
623
|
+
|
624
|
+
# Returns the coverage executable path.
|
625
|
+
#
|
626
|
+
# @return [String] the path
|
627
|
+
#
|
628
|
+
def coverage_bin
|
629
|
+
@coverage_bin ||= which 'istanbul'
|
630
|
+
end
|
631
|
+
|
632
|
+
# Get the coverage file to save all coverage data.
|
633
|
+
# Creates `tmp/coverage` if not exists.
|
634
|
+
#
|
635
|
+
# @return [String] the filename to use
|
636
|
+
#
|
637
|
+
def coverage_file
|
638
|
+
File.join(coverage_root, 'coverage.json')
|
639
|
+
end
|
640
|
+
|
641
|
+
# Create and returns the coverage root directory.
|
642
|
+
#
|
643
|
+
# @return [String] the coverage root
|
644
|
+
#
|
645
|
+
def coverage_root
|
646
|
+
File.expand_path(File.join('tmp', 'coverage'))
|
647
|
+
end
|
648
|
+
|
649
|
+
# Creates and returns the coverage report directory.
|
650
|
+
#
|
651
|
+
# @return [String] the coverage report directory
|
652
|
+
#
|
653
|
+
def coverage_report_directory
|
654
|
+
File.expand_path(options[:coverage_html_dir])
|
655
|
+
end
|
670
656
|
end
|
671
657
|
end
|
672
658
|
end
|
673
|
-
|